Code cleanup and documentation

This commit is contained in:
Landon Wark
2024-02-09 14:03:35 -06:00
parent eda62fba5a
commit a534d229a8
30 changed files with 1558 additions and 1347 deletions

View File

@@ -2,5 +2,3 @@
Contains all operations for creating charts, graphs and visual effects.
'''
from .control_charts import *
from .barcode import *
from .plate_map import *

View File

@@ -1,19 +0,0 @@
from reportlab.graphics.barcode import createBarcodeImageInMemory
from reportlab.graphics.shapes import Drawing
from reportlab.lib.units import mm
def make_plate_barcode(text:str, width:int=100, height:int=25) -> Drawing:
"""
Creates a barcode image for a given str.
Args:
text (str): Input string
width (int, optional): Width (pixels) of image. Defaults to 100.
height (int, optional): Height (pixels) of image. Defaults to 25.
Returns:
Drawing: image object
"""
# return createBarcodeDrawing('Code128', value=text, width=200, height=50, humanReadable=True)
return createBarcodeImageInMemory('Code128', value=text, width=width*mm, height=height*mm, humanReadable=True, format="png")

View File

@@ -12,7 +12,6 @@ from frontend.widgets.functions import select_save_file
logger = logging.getLogger(f"submissions.{__name__}")
def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
"""
Constructs figures based on parsed pandas dataframe.
@@ -40,7 +39,6 @@ def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure
genera.append("")
df['genus'] = df['genus'].replace({'\*':''}, regex=True).replace({"NaN":"Unknown"})
df['genera'] = genera
# df = df.dropna()
# remove original runs, using reruns if applicable
df = drop_reruns_from_df(ctx=ctx, df=df)
# sort by and exclude from
@@ -224,4 +222,4 @@ def construct_html(figure:Figure) -> str:
else:
html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>'
return html
return html

View File

@@ -1,121 +0,0 @@
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from tools import check_if_app, jinja_template_loading
import logging, sys
logger = logging.getLogger(f"submissions.{__name__}")
def make_plate_map(sample_list:list) -> Image:
"""
Makes a pillow image of a plate from hitpicks
Args:
sample_list (list): list of sample dictionaries from the hitpicks
Returns:
Image: Image of the 96 well plate with positive samples in red.
"""
# If we can't get a plate number, do nothing
try:
plate_num = sample_list[0]['plate_name']
except IndexError as e:
logger.error(f"Couldn't get a plate number. Will not make plate.")
return None
except TypeError as e:
logger.error(f"No samples for this plate. Nothing to do.")
return None
# Make an 8 row, 12 column, 3 color ints array, filled with white by default
grid = np.full((8,12,3),255, dtype=np.uint8)
# Go through samples and change its row/column to red if positive, else blue
for sample in sample_list:
logger.debug(f"sample keys: {list(sample.keys())}")
# set color of square
if sample['positive']:
colour = [255,0,0]
else:
if 'colour' in sample.keys():
colour = sample['colour']
else:
colour = [0,0,255]
grid[int(sample['row'])-1][int(sample['column'])-1] = colour
# Create pixel image from the grid and enlarge
img = Image.fromarray(grid).resize((1200, 800), resample=Image.NEAREST)
# create a drawer over the image
draw = ImageDraw.Draw(img)
# draw grid over the image
y_start = 0
y_end = img.height
step_size = int(img.width / 12)
for x in range(0, img.width, step_size):
line = ((x, y_start), (x, y_end))
draw.line(line, fill=128)
x_start = 0
x_end = img.width
step_size = int(img.height / 8)
for y in range(0, img.height, step_size):
line = ((x_start, y), (x_end, y))
draw.line(line, fill=128)
del draw
old_size = img.size
new_size = (1300, 900)
# create a new, larger white image to hold the annotations
new_img = Image.new("RGB", new_size, "White")
box = tuple((n - o) // 2 for n, o in zip(new_size, old_size))
# paste plate map into the new image
new_img.paste(img, box)
# create drawer over the new image
draw = ImageDraw.Draw(new_img)
if check_if_app():
font_path = Path(sys._MEIPASS).joinpath("files", "resources")
else:
font_path = Path(__file__).parents[2].joinpath('resources').absolute()
logger.debug(f"Font path: {font_path}")
font = ImageFont.truetype(font_path.joinpath('arial.ttf').__str__(), 32)
row_dict = ["A", "B", "C", "D", "E", "F", "G", "H"]
# write the plate number on the image
draw.text((100, 850),plate_num,(0,0,0),font=font)
# write column numbers
for num in range(1,13):
x = (num * 100) - 10
draw.text((x, 0), str(num), (0,0,0),font=font)
# write row letters
for num in range(1,9):
letter = row_dict[num-1]
y = (num * 100) - 10
draw.text((10, y), letter, (0,0,0),font=font)
return new_img
def make_plate_map_html(sample_list:list, plate_rows:int=8, plate_columns=12) -> str:
"""
Constructs an html based plate map.
Args:
sample_list (list): List of submission samples
plate_rows (int, optional): Number of rows in the plate. Defaults to 8.
plate_columns (int, optional): Number of columns in the plate. Defaults to 12.
Returns:
str: html output string.
"""
for sample in sample_list:
if sample['positive']:
sample['background_color'] = "#f10f07"
else:
if "colour" in sample.keys():
sample['background_color'] = "#69d84f"
else:
sample['background_color'] = "#80cbc4"
output_samples = []
for column in range(1, plate_columns+1):
for row in range(1, plate_rows+1):
try:
well = [item for item in sample_list if item['row'] == row and item['column']==column][0]
except IndexError:
well = dict(name="", row=row, column=column, background_color="#ffffff")
output_samples.append(well)
env = jinja_template_loading()
template = env.get_template("plate_map.html")
html = template.render(samples=output_samples, PLATE_ROWS=plate_rows, PLATE_COLUMNS=plate_columns)
return html

View File

@@ -52,7 +52,6 @@ class App(QMainWindow):
self._createMenuBar()
self._createToolBar()
self._connectActions()
# self._controls_getter()
self.show()
self.statusBar().showMessage('Ready', 5000)
@@ -114,14 +113,10 @@ class App(QMainWindow):
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)
self.searchLog.triggered.connect(self.runSearch)
def showAbout(self):

View File

@@ -4,9 +4,9 @@ from PyQt6.QtWidgets import (
QDateEdit, QLabel, QSizePolicy
)
from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control#, get_control_subtypes
from backend.db import ControlType, Control
from PyQt6.QtCore import QDate, QSize
import logging, sys
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
@@ -88,9 +88,7 @@ class ControlsViewer(QWidget):
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 = ControlType.query(name=self.con_type).get_subtypes(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:
@@ -103,7 +101,6 @@ class ControlsViewer(QWidget):
self.chart_maker()
self.report.add_result(report)
def chart_maker_function(self):
"""
Create html chart for controls reporting

View File

@@ -2,9 +2,10 @@ from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QHBoxLayout,
QVBoxLayout, QDialogButtonBox)
from backend.db.models import SubmissionType, Equipment, BasicSubmission
from backend.db.models import Equipment, BasicSubmission
from backend.validators.pydant import PydEquipment, PydEquipmentRole
import logging
from typing import List
logger = logging.getLogger(f"submissions.{__name__}")
@@ -24,19 +25,29 @@ class EquipmentUsage(QDialog):
self.populate_form()
def populate_form(self):
"""
Create form widgets
"""
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
label = self.LabelRow(parent=self)
self.layout.addWidget(label)
# logger.debug("Creating widgets for equipment")
for eq in self.opt_equipment:
widg = eq.toForm(parent=self, submission_type=self.submission.submission_type, used=self.used_equipment)
widg = eq.toForm(parent=self, used=self.used_equipment)
self.layout.addWidget(widg)
widg.update_processes()
self.layout.addWidget(self.buttonBox)
def parse_form(self):
def parse_form(self) -> List[PydEquipment]:
"""
Pull info from all RoleComboBox widgets
Returns:
List[PydEquipment]: All equipment pulled from widgets
"""
output = []
for widget in self.findChildren(QWidget):
match widget:
@@ -63,43 +74,18 @@ class EquipmentUsage(QDialog):
self.setLayout(self.layout)
def check_all(self):
"""
Toggles all checkboxes in the form
"""
for object in self.parent().findChildren(QCheckBox):
object.setChecked(self.check.isChecked())
class EquipmentCheckBox(QWidget):
def __init__(self, parent, equipment:PydEquipment) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
self.label = QLabel()
self.label.setMaximumWidth(125)
self.label.setMinimumWidth(125)
self.check = QCheckBox()
if equipment.static:
self.check.setChecked(True)
if equipment.nickname != None:
text = f"{equipment.name} ({equipment.nickname})"
else:
text = equipment.name
self.setObjectName(equipment.name)
self.label.setText(text)
self.layout.addWidget(self.label)
self.layout.addWidget(self.check)
self.setLayout(self.layout)
def parse_form(self) -> str|None:
if self.check.isChecked():
return self.objectName()
else:
return None
# TODO: Figure out how this is working again
class RoleComboBox(QWidget):
def __init__(self, parent, role:PydEquipmentRole, submission_type:SubmissionType, used:list) -> None:
def __init__(self, parent, role:PydEquipmentRole, used:list) -> None:
super().__init__(parent)
self.layout = QHBoxLayout()
# label = QLabel()
# label.setText(pool.name)
self.role = role
self.check = QCheckBox()
if role.name in used:
@@ -111,14 +97,10 @@ class RoleComboBox(QWidget):
self.box.setMinimumWidth(200)
self.box.addItems([item.name for item in role.equipment])
self.box.currentTextChanged.connect(self.update_processes)
# self.check = QCheckBox()
# self.layout.addWidget(label)
self.process = QComboBox()
self.process.setMaximumWidth(200)
self.process.setMinimumWidth(200)
self.process.setEditable(True)
# self.process.addItems(submission_type.get_processes_for_role(equipment_role=role.name))
# self.process.addItems(role.processes)
self.layout.addWidget(self.check)
label = QLabel(f"{role.name}:")
label.setMinimumWidth(200)
@@ -127,11 +109,12 @@ class RoleComboBox(QWidget):
self.layout.addWidget(label)
self.layout.addWidget(self.box)
self.layout.addWidget(self.process)
# self.layout.addWidget(self.check)
self.setLayout(self.layout)
# self.update_processes()
def update_processes(self):
"""
Changes processes when equipment is changed
"""
equip = self.box.currentText()
logger.debug(f"Updating equipment: {equip}")
equip2 = [item for item in self.role.equipment if item.name==equip][0]
@@ -139,10 +122,16 @@ class RoleComboBox(QWidget):
self.process.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.processes])
def parse_form(self) -> str|None:
def parse_form(self) -> PydEquipment|None:
"""
Creates PydEquipment for values in form
Returns:
PydEquipment|None: PydEquipment matching form
"""
eq = Equipment.query(name=self.box.currentText())
# if self.check.isChecked():
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
# else:
# return None
try:
return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname)
except Exception as e:
logger.error(f"Could create PydEquipment due to: {e}")

View File

@@ -1,7 +1,7 @@
# import required modules
# from PyQt6.QtCore import Qt
"""
Gel box for artic quality control
"""
from PyQt6.QtWidgets import *
# import sys
from PyQt6.QtWidgets import QWidget
import numpy as np
import pyqtgraph as pg
@@ -9,11 +9,17 @@ from PyQt6.QtGui import *
from PyQt6.QtCore import *
from PIL import Image
import numpy as np
import logging
from pprint import pformat
from typing import Tuple, List
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
# Main window class
class GelBox(QDialog):
def __init__(self, parent, img_path):
def __init__(self, parent, img_path:str|Path):
super().__init__(parent)
# setting title
self.setWindowTitle("PyQtGraph")
@@ -27,11 +33,12 @@ class GelBox(QDialog):
# calling method
self.UiComponents()
# showing all the widgets
# self.show()
# method for components
def UiComponents(self):
# widget = QWidget()
"""
Create widgets in ui
"""
# setting configuration options
pg.setConfigOptions(antialias=True)
# creating image view object
@@ -39,33 +46,41 @@ class GelBox(QDialog):
img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0]))
layout = QGridLayout()
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
self.core_number = QLineEdit()
layout.addWidget(self.core_number, 0,2)
# setting this layout to the widget
# widget.setLayout(layout)
# plot window goes on right side, spanning 3 rows
layout.addWidget(self.imv, 0, 0,20,20)
layout.addWidget(self.imv, 1, 1,20,20)
# setting this widget as central widget of the main window
self.form = ControlsForm(parent=self)
layout.addWidget(self.form,21,1,1,4)
layout.addWidget(self.form,22,1,1,4)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 21, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
# self.buttonBox.clicked.connect(self.submit)
layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
self.setLayout(layout)
def parse_form(self):
return self.img_path, self.form.parse_form()
def parse_form(self) -> Tuple[str, str|Path, list]:
"""
Get relevant values from self/form
Returns:
Tuple[str, str|Path, list]: output values
"""
dna_core_submission_number = self.core_number.text()
return dna_core_submission_number, self.img_path, self.form.parse_form()
class ControlsForm(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QGridLayout()
columns = []
rows = []
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
label = QLabel(item)
self.layout.addWidget(label, 0, iii,1,1)
if iii > 1:
@@ -85,11 +100,22 @@ class ControlsForm(QWidget):
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
self.setLayout(self.layout)
def parse_form(self):
dicto = {}
def parse_form(self) -> List[dict]:
"""
Pulls the controls statuses from the form.
Returns:
List[dict]: output of values
"""
output = []
for le in self.findChildren(QLineEdit):
label = [item.strip() for item in le.objectName().split(" : ")]
if label[0] not in dicto.keys():
dicto[label[0]] = {}
dicto[label[0]][label[1]] = le.text()
return dicto
try:
dicto = [item for item in output if item['name']==label[0]][0]
except IndexError:
dicto = dict(name=label[0], values=[])
dicto['values'].append(dict(name=label[1], value=le.text()))
if label[0] not in [item['name'] for item in output]:
output.append(dicto)
logger.debug(pformat(output))
return output

View File

@@ -79,12 +79,10 @@ class KitAdder(QWidget):
"""
insert new reagent type row
"""
print(self.app)
# get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
self.grid.addWidget(reg_form, maxrow,0,1,4)
def submit(self) -> None:
@@ -118,6 +116,12 @@ class KitAdder(QWidget):
self.__init__(self.parent())
def parse_form(self) -> Tuple[dict, list]:
"""
Pulls reagent and general info from form
Returns:
Tuple[dict, list]: dict=info, list=reagents
"""
logger.debug(f"Hello from {self.__class__} parser!")
info = {}
reagents = []
@@ -188,10 +192,19 @@ class ReagentTypeForm(QWidget):
]
def remove(self):
"""
Destroys this row of reagenttype from the form
"""
self.setParent(None)
self.destroy()
def parse_form(self) -> dict:
"""
Pulls ReagentType info from the form.
Returns:
dict: _description_
"""
logger.debug(f"Hello from {self.__class__} parser!")
info = {}
info['eol'] = self.eol.value()

View File

@@ -25,7 +25,6 @@ class AddReagentForm(QDialog):
"""
def __init__(self, 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
@@ -41,7 +40,6 @@ class AddReagentForm(QDialog):
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)
@@ -56,7 +54,6 @@ class AddReagentForm(QDialog):
# 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?
@@ -169,7 +166,13 @@ class FirstStrandSalvage(QDialog):
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
def parse_form(self) -> dict:
"""
Pulls first strand info from form.
Returns:
dict: Output info
"""
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 LogParser(QDialog):
@@ -193,9 +196,15 @@ class LogParser(QDialog):
def filelookup(self):
"""
Select file to search
"""
self.fname = select_open_file(self, "tabular")
def runsearch(self):
"""
Gets total/percent occurences of string in tabular file.
"""
count: int = 0
total: int = 0
logger.debug(f"Current search term: {self.phrase_looker.currentText()}")

View File

@@ -47,7 +47,7 @@ class AlertPop(QMessageBox):
class KitSelector(QDialog):
"""
dialog to ask yes/no questions
dialog to input KitType manually
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
@@ -69,12 +69,18 @@ class KitSelector(QDialog):
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def getValues(self):
def getValues(self) -> str:
"""
Get KitType(str) from widget
Returns:
str: KitType as str
"""
return self.widget.currentText()
class SubmissionTypeSelector(QDialog):
"""
dialog to ask yes/no questions
dialog to input SubmissionType manually
"""
def __init__(self, title:str, message:str) -> QDialog:
super().__init__()
@@ -97,5 +103,11 @@ class SubmissionTypeSelector(QDialog):
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
def parse_form(self) -> str:
"""
Pulls SubmissionType(str) from widget
Returns:
str: SubmissionType as str
"""
return self.widget.currentText()

View File

@@ -1,25 +1,25 @@
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
QLabel, QDialogButtonBox, QToolBar, QTextEdit)
from PyQt6.QtGui import QAction, QPixmap
QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt
from PyQt6 import QtPrintSupport
from backend.db.models import BasicSubmission
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from tools import check_if_app, jinja_template_loading
from tools import check_if_app
from .functions import select_save_file
from io import BytesIO
from tempfile import TemporaryFile, TemporaryDirectory
from pathlib import Path
from xhtml2pdf import pisa
import logging, base64
from getpass import getuser
from datetime import datetime
from pprint import pformat
from html2image import Html2Image
from PIL import Image
from typing import List
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
@@ -27,7 +27,6 @@ class SubmissionDetails(QDialog):
def __init__(self, parent, sub:BasicSubmission) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent()
except AttributeError:
@@ -36,19 +35,16 @@ class SubmissionDetails(QDialog):
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# sub = BasicSubmission.query(id=id)
self.base_dict = sub.to_dict(full_data=True)
logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k != 'samples'})}")
# 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()
self.base_dict['barcode'] = base64.b64encode(sub.make_plate_barcode(width=120, height=30)).decode('utf-8')
logger.debug(f"Making platemap...")
self.base_dict['platemap'] = make_plate_map_html(self.plate_dicto)
self.template = env.get_template("submission_details.html")
self.base_dict['platemap'] = sub.make_plate_map()
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
self.html = self.template.render(sub=self.base_dict)
webview = QWebEngineView()
webview.setMinimumSize(900, 500)
@@ -63,21 +59,29 @@ class SubmissionDetails(QDialog):
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.
"""
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()
temp_dir = Path(TemporaryDirectory().name)
hti = Html2Image(output_path=temp_dir, size=(1200, 750))
temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name)
screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
export_map = Image.open(screenshot[0])
export_map = export_map.convert('RGB')
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')
del self.base_dict['platemap']
self.html2 = self.template.render(sub=self.base_dict)
with open("test.html", "w") as fw:
fw.write(self.html2)
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(self.html2, dest=f)
@@ -88,73 +92,6 @@ class SubmissionDetails(QDialog):
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):
"""
@@ -163,7 +100,6 @@ class SubmissionComment(QDialog):
def __init__(self, parent, submission:BasicSubmission) -> None:
super().__init__(parent)
# self.ctx = ctx
try:
self.app = parent.parent().parent().parent().parent().parent().parent
print(f"App: {self.app}")
@@ -185,7 +121,7 @@ class SubmissionComment(QDialog):
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def parse_form(self):
def parse_form(self) -> List[dict]:
"""
Adds comment to submission object.
"""

View File

@@ -1,37 +1,22 @@
'''
Contains widgets specific to the submission summary and submission details.
'''
import base64, logging, json
from datetime import datetime
from io import BytesIO
import logging, json
from pprint import pformat
from PyQt6 import QtPrintSupport
from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea,
QMessageBox, QMenu, QLabel,
QDialogButtonBox, QToolBar
)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QTableView, QMenu
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.models import BasicSubmission, Equipment
from PyQt6.QtGui import QAction, QCursor
from backend.db.models import BasicSubmission
from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
from tools import Report, Result, get_first_blank_df_row, row_map
from xhtml2pdf import pisa
from .pop_ups import QuestionAsker
from .equipment_usage import EquipmentUsage
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker
import pandas as pd
from openpyxl.worksheet.worksheet import Worksheet
from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}")
env = jinja_template_loading()
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
@@ -89,20 +74,17 @@ class SubmissionsSheet(QTableView):
"""
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)
self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
def setData(self) -> None:
"""
sets data in model
"""
# self.data = submissions_to_df()
self.data = BasicSubmission.submissions_to_df()
try:
self.data['id'] = self.data['id'].apply(str)
@@ -114,39 +96,6 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
# def show_details(self, submission:BasicSubmission) -> 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, sub=submission)
# 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(parent=self, rsl=value)
if dlg.exec():
dlg.add_comment()
def contextMenuEvent(self, event):
"""
Creates actions for right click menu events.
@@ -158,21 +107,6 @@ class SubmissionsSheet(QTableView):
id = id.sibling(id.row(),0).data()
submission = BasicSubmission.query(id=id)
self.menu = QMenu(self)
# renameAction = QAction('Delete', self)
# detailsAction = QAction('Details', self)
# commentAction = QAction("Add Comment", self)
# equipAction = QAction("Add Equipment", self)
# backupAction = QAction("Export", self)
# renameAction.triggered.connect(lambda: self.delete_item(submission))
# detailsAction.triggered.connect(lambda: self.show_details(submission))
# commentAction.triggered.connect(lambda: self.add_comment(submission))
# backupAction.triggered.connect(lambda: self.regenerate_submission_form(submission))
# equipAction.triggered.connect(lambda: self.add_equipment(submission))
# self.menu.addAction(detailsAction)
# self.menu.addAction(renameAction)
# self.menu.addAction(commentAction)
# self.menu.addAction(backupAction)
# self.menu.addAction(equipAction)
self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys():
logger.debug(f"Adding {k}")
@@ -183,57 +117,21 @@ class SubmissionsSheet(QTableView):
self.menu.popup(QCursor.pos())
def triggered_action(self, action_name:str):
"""
Calls the triggered action from the context menu
Args:
action_name (str): name of the action from the menu
"""
logger.debug(f"Action: {action_name}")
logger.debug(f"Responding with {self.con_actions[action_name]}")
func = self.con_actions[action_name]
func(obj=self)
def add_equipment(self):
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),0).data()
self.add_equipment_function(rsl_plate_id=value)
def add_equipment_function(self, submission:BasicSubmission):
# submission = BasicSubmission.query(id=rsl_plate_id)
submission_type = submission.submission_type_name
dlg = EquipmentUsage(parent=self, submission_type=submission_type, submission=submission)
if dlg.exec():
equipment = dlg.parse_form()
logger.debug(f"We've got equipment: {equipment}")
for equip in equipment:
e = Equipment.query(name=equip.name)
# assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# process = Process.query(name=equip.processes)
# assoc.process = process
# assoc.role = equip.role
_, assoc = equip.toSQL(submission=submission)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
else:
pass
def delete_item(self, submission:BasicSubmission):
"""
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")
msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {submission.rsl_plate_num}?\n")
if msg.exec():
# delete_submission(id=value)
submission.delete()
else:
return
self.setData()
def link_extractions(self):
"""
Pull extraction logs into the db
"""
self.link_extractions_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -306,6 +204,9 @@ class SubmissionsSheet(QTableView):
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def link_pcr(self):
"""
Pull pcr logs into the db
"""
self.link_pcr_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -376,6 +277,9 @@ class SubmissionsSheet(QTableView):
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
def generate_report(self):
"""
Make a report
"""
self.generate_report_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -436,12 +340,3 @@ class SubmissionsSheet(QTableView):
cell.style = 'Currency'
writer.close()
self.report.add_result(report)
def regenerate_submission_form(self, submission:BasicSubmission):
# index = (self.selectionModel().currentIndex())
# value = index.sibling(index.row(),0).data()
# logger.debug(index)
# sub = BasicSubmission.query(id=value)
fname = select_save_file(self, default_name=submission.to_pydantic().construct_filename(), extension="xlsx")
submission.backup(fname=fname, full_backup=False)

View File

@@ -2,21 +2,14 @@ from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QDateEdit
QLineEdit, QSpinBox
)
from sqlalchemy import FLOAT, INTEGER
from sqlalchemy.orm.attributes import InstrumentedAttribute
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentRoleAssociation, BasicSubmission
from backend.validators import PydReagentType, PydKit
from backend.db import SubmissionType, BasicSubmission
import logging
from pprint import pformat
from tools import Report
from typing import Tuple
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionTypeAdder(QWidget):
@@ -46,35 +39,21 @@ class SubmissionTypeAdder(QWidget):
self.grid.addWidget(template_selector,3,1)
self.template_label = QLabel("None")
self.grid.addWidget(self.template_label,3,2)
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost']
self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)}
self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude}
for iii, key in enumerate(self.columns):
idx = iii + 4
# convert field name to human readable.
# field_name = key
# self.grid.addWidget(QLabel(field_name),idx,0)
# print(self.columns[key].type)
# match self.columns[key].type:
# case FLOAT():
# add_widget = QDoubleSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case INTEGER():
# add_widget = QSpinBox()
# add_widget.setMinimum(0)
# add_widget.setMaximum(9999)
# case _:
# add_widget = QLineEdit()
# add_widget.setObjectName(key)
self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
scroll.setWidget(scrollContent)
self.submit_btn.clicked.connect(self.submit)
template_selector.clicked.connect(self.get_template_path)
def submit(self):
"""
Create SubmissionType and send to db
"""
info = self.parse_form()
ST = SubmissionType(name=self.st_name.text(), info_map=info)
try:
@@ -84,11 +63,20 @@ class SubmissionTypeAdder(QWidget):
logger.error(f"Could not find template file: {self.template_path}")
ST.save(ctx=self.app.ctx)
def parse_form(self):
def parse_form(self) -> dict:
"""
Pulls info from form
Returns:
dict: information from form
"""
widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
return {widget.objectName():widget.parse_form() for widget in widgets}
def get_template_path(self):
"""
Sets path for loading a submission form template
"""
self.template_path = select_open_file(obj=self, file_extension="xlsx")
self.template_label.setText(self.template_path.__str__())
@@ -113,7 +101,13 @@ class InfoWidget(QWidget):
self.column.setObjectName("column")
grid.addWidget(self.column,2,3)
def parse_form(self):
def parse_form(self) -> dict:
"""
Pulls info from the Info form.
Returns:
dict: sheets, row, column
"""
return dict(
sheets = self.sheet.text().split(","),
row = self.row.value(),

View File

@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import pyqtSignal
from pathlib import Path
from . import select_open_file, select_save_file
import logging, difflib, inspect, json, sys
import logging, difflib, inspect, json
from pathlib import Path
from tools import Report, Result, check_not_nan
from backend.excel.parser import SheetParser, PCRParser
@@ -16,7 +16,6 @@ from backend.db import (
)
from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop
# from .misc import ReagentFormWidget
from typing import List, Tuple
from datetime import date
@@ -24,6 +23,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionFormContainer(QWidget):
# A signal carrying a path
import_drag = pyqtSignal(Path)
def __init__(self, parent: QWidget) -> None:
@@ -31,19 +31,24 @@ class SubmissionFormContainer(QWidget):
super().__init__(parent)
self.app = self.parent().parent()
self.report = Report()
# self.parent = parent
self.setAcceptDrops(True)
# if import_drag is emitted, importSubmission will fire
self.import_drag.connect(self.importSubmission)
def dragEnterEvent(self, event):
"""
Allow drag if file.
"""
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Sets filename when file dropped
"""
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)
@@ -52,7 +57,6 @@ class SubmissionFormContainer(QWidget):
"""
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)
@@ -62,6 +66,9 @@ class SubmissionFormContainer(QWidget):
self.app.result_reporter()
def scrape_reagents(self, *args, **kwargs):
"""
Called when a reagent is changed.
"""
caller = inspect.stack()[1].function.__repr__().replace("'", "")
logger.debug(f"Args: {args}, kwargs: {kwargs}")
self.scrape_reagents_function(args[0], caller=caller)
@@ -80,7 +87,6 @@ class SubmissionFormContainer(QWidget):
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()
@@ -94,14 +100,12 @@ class SubmissionFormContainer(QWidget):
"""
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):
@@ -116,12 +120,11 @@ class SubmissionFormContainer(QWidget):
"""
logger.debug(f"\n\nStarting Import...\n\n")
report = Report()
# logger.debug(obj.ctx)
# initialize samples
try:
self.form.setParent(None)
except AttributeError:
pass
# initialize samples
self.samples = []
self.missing_info = []
# set file dialog
@@ -129,7 +132,6 @@ class SubmissionFormContainer(QWidget):
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
@@ -141,14 +143,9 @@ class SubmissionFormContainer(QWidget):
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
@@ -176,11 +173,8 @@ class SubmissionFormContainer(QWidget):
"""
self.form.reagents = []
logger.debug(f"\n\n{caller}\n\n")
# assert caller == "import_submission_function"
report = Report()
logger.debug(f"Extraction kit: {extraction_kit}")
# obj.reagents = []
# obj.missing_reagents = []
# Remove previous reagent widgets
try:
old_reagents = self.form.find_widgets()
@@ -191,14 +185,6 @@ class SubmissionFormContainer(QWidget):
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)
match caller:
case "import_submission_function":
self.form.reagents = self.prsr.sub['reagents']
@@ -231,11 +217,9 @@ class SubmissionFormContainer(QWidget):
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:
logger.debug(f"Creating widget for {reagent}")
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
# add_widget.setParent(sub_form_container.form)
self.form.layout().addWidget(add_widget)
if reagent.missing:
missing_reagents.append(reagent)
@@ -275,7 +259,6 @@ class SubmissionFormContainer(QWidget):
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)
result = self.pyd.check_kit_integrity()
report.add_result(result)
if len(result.results) > 0:
@@ -283,7 +266,6 @@ class SubmissionFormContainer(QWidget):
return
base_submission, result = self.pyd.toSQL()
# logger.debug(f"Base submission: {base_submission.to_dict()}")
# sys.exit()
# check output message for issues
match result.code:
# code 0: everything is fine.
@@ -309,9 +291,7 @@ class SubmissionFormContainer(QWidget):
# add reagents to submission object
for reagent in base_submission.reagents:
# logger.debug(f"Updating: {reagent} with {reagent.lot}")
# update_last_used(reagent=reagent, kit=base_submission.extraction_kit)
reagent.update_last_used(kit=base_submission.extraction_kit)
# sys.exit()
# 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.")
@@ -324,24 +304,15 @@ class SubmissionFormContainer(QWidget):
# 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):
"""
Save the submission's csv file.
Args:
fname (Path | None, optional): Input filename. Defaults to None.
"""
if isinstance(fname, bool) or fname == None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
try:
@@ -351,6 +322,9 @@ class SubmissionFormContainer(QWidget):
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
def import_pcr_results(self):
"""
Pull QuantStudio results into db
"""
self.import_pcr_results_function()
self.app.report.add_result(self.report)
self.report = Report()
@@ -370,7 +344,6 @@ class SubmissionFormContainer(QWidget):
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}")
@@ -378,14 +351,11 @@ class SubmissionFormContainer(QWidget):
# 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
@@ -407,7 +377,6 @@ class SubmissionFormContainer(QWidget):
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(original=False)
@@ -419,18 +388,13 @@ class SubmissionFormContainer(QWidget):
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)
sub.update_subsampassoc(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', 'comment', 'equipment']
self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
layout = QVBoxLayout()
@@ -441,25 +405,53 @@ class SubmissionFormWidget(QWidget):
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):
def create_widget(self, key:str, value:dict, submission_type:str|None=None) -> "self.InfoItem":
"""
Make an InfoItem widget to hold a field
Args:
key (str): Name of the field
value (dict): Value of field
submission_type (str | None, optional): Submissiontype as str. Defaults to None.
Returns:
self.InfoItem: Form widget to hold name:value
"""
if key not in self.ignore:
return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
return None
def clear_form(self):
"""
Removes all form widgets
"""
for item in self.findChildren(QWidget):
item.setParent(None)
def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
"""
Gets all widgets filtered by object name
Args:
object_name (str | None, optional): name to filter by. Defaults to None.
Returns:
List[QWidget]: Widgets matching filter
"""
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:
"""
Transforms form info into PydSubmission
Returns:
PydSubmission: Pydantic submission object
"""
logger.debug(f"Hello from form parser!")
info = {}
reagents = []
@@ -483,8 +475,6 @@ class SubmissionFormWidget(QWidget):
value = getattr(self, item)
logger.debug(f"Setting {item}")
info[item] = value
# app = self.parent().parent().parent().parent().parent().parent().parent().parent
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
submission = PydSubmission(reagents=reagents, **info)
return submission
@@ -513,7 +503,13 @@ class SubmissionFormWidget(QWidget):
case QLineEdit():
self.input.textChanged.connect(self.update_missing)
def parse_form(self):
def parse_form(self) -> Tuple[str, dict]:
"""
Pulls info from widget into dict
Returns:
Tuple[str, dict]: name of field, {value, missing}
"""
match self.input:
case QLineEdit():
value = self.input.text()
@@ -526,6 +522,18 @@ class SubmissionFormWidget(QWidget):
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:
"""
Creates form widget
Args:
parent (QWidget): parent widget
key (str): name of field
value (dict): value, and is it missing from scrape
submission_type (str | None, optional): SubmissionType as str. Defaults to None.
Returns:
QWidget: Form object
"""
try:
value = value['value']
except (TypeError, KeyError):
@@ -565,7 +573,6 @@ class SubmissionFormWidget(QWidget):
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)
@@ -578,7 +585,6 @@ class SubmissionFormWidget(QWidget):
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)))
@@ -593,10 +599,12 @@ class SubmissionFormWidget(QWidget):
if add_widget != None:
add_widget.setObjectName(key)
add_widget.setParent(parent)
return add_widget
def update_missing(self):
"""
Set widget status to updated
"""
self.missing = True
self.label.updated(self.objectName())
@@ -622,6 +630,13 @@ class SubmissionFormWidget(QWidget):
self.setText(f"MISSING {output}")
def updated(self, key:str, title:bool=True):
"""
Mark widget as updated
Args:
key (str): Name of the field
title (bool, optional): Use title case. Defaults to True.
"""
if title:
output = key.replace('_', ' ').title()
else:
@@ -632,12 +647,9 @@ class ReagentFormWidget(QWidget):
def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
super().__init__(parent)
# self.setParent(parent)
self.app = self.parent().parent().parent().parent().parent().parent().parent().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)
@@ -652,14 +664,18 @@ class ReagentFormWidget(QWidget):
self.lot.currentTextChanged.connect(self.updated)
def parse_form(self) -> Tuple[PydReagent, dict]:
"""
Pulls form info into PydReagent
Returns:
Tuple[PydReagent, dict]: PydReagent and Report(?)
"""
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():
print(self.app)
wanted_reagent = self.app.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
return wanted_reagent, None
else:
@@ -669,15 +685,15 @@ class ReagentFormWidget(QWidget):
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):
"""
Set widget status to updated
"""
self.missing = True
self.label.updated(self.reagent.type)
@@ -696,19 +712,21 @@ class ReagentFormWidget(QWidget):
self.setText(f"MISSING {reagent.type}")
def updated(self, reagent_type:str):
"""
Marks widget as updated
Args:
reagent_type (str): _description_
"""
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 = [str(item.lot) for item in lookup]
output_reg = []
@@ -726,11 +744,8 @@ class ReagentFormWidget(QWidget):
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
@@ -752,4 +767,3 @@ class ReagentFormWidget(QWidget):
logger.debug(f"New relevant reagents: {relevant_reagents}")
self.setObjectName(f"lot_{reagent.type}")
self.addItems(relevant_reagents)