Increased robustness of form parsers.
This commit is contained in:
@@ -1,22 +1,26 @@
|
||||
'''
|
||||
Constructs main application.
|
||||
'''
|
||||
from pprint import pformat
|
||||
import sys
|
||||
from typing import Tuple
|
||||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QToolBar,
|
||||
QTabWidget, QWidget, QVBoxLayout,
|
||||
QComboBox, QHBoxLayout,
|
||||
QScrollArea
|
||||
QScrollArea, QLineEdit, QDateEdit,
|
||||
QSpinBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from pathlib import Path
|
||||
from backend.db import (
|
||||
construct_reagent, store_object, lookup_control_types, lookup_modes
|
||||
)
|
||||
from .all_window_functions import extract_form_info
|
||||
# from .all_window_functions import extract_form_info
|
||||
from tools import check_if_app, Settings
|
||||
from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker
|
||||
from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker, ImportReagent
|
||||
import logging
|
||||
from datetime import date
|
||||
import webbrowser
|
||||
@@ -51,7 +55,9 @@ class App(QMainWindow):
|
||||
self._createToolBar()
|
||||
self._connectActions()
|
||||
self._controls_getter()
|
||||
# self.status_bar = self.statusBar()
|
||||
self.show()
|
||||
self.statusBar().showMessage('Ready', 5000)
|
||||
|
||||
|
||||
def _createMenuBar(self):
|
||||
@@ -73,7 +79,7 @@ class App(QMainWindow):
|
||||
fileMenu.addAction(self.importPCRAction)
|
||||
methodsMenu.addAction(self.constructFS)
|
||||
reportMenu.addAction(self.generateReportAction)
|
||||
maintenanceMenu.addAction(self.joinControlsAction)
|
||||
# maintenanceMenu.addAction(self.joinControlsAction)
|
||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||
maintenanceMenu.addAction(self.joinPCRAction)
|
||||
|
||||
@@ -99,7 +105,7 @@ class App(QMainWindow):
|
||||
self.generateReportAction = QAction("Make Report", self)
|
||||
self.addKitAction = QAction("Import Kit", self)
|
||||
self.addOrgAction = QAction("Import Org", self)
|
||||
self.joinControlsAction = QAction("Link Controls")
|
||||
# self.joinControlsAction = QAction("Link Controls")
|
||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||
self.joinPCRAction = QAction("Link PCR Logs")
|
||||
self.helpAction = QAction("&About", self)
|
||||
@@ -122,7 +128,7 @@ class App(QMainWindow):
|
||||
self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter)
|
||||
self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter)
|
||||
self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
|
||||
self.joinControlsAction.triggered.connect(self.linkControls)
|
||||
# self.joinControlsAction.triggered.connect(self.linkControls)
|
||||
self.joinExtractionAction.triggered.connect(self.linkExtractions)
|
||||
self.joinPCRAction.triggered.connect(self.linkPCR)
|
||||
self.helpAction.triggered.connect(self.showAbout)
|
||||
@@ -149,6 +155,7 @@ class App(QMainWindow):
|
||||
webbrowser.get('windows-default').open(f"file://{url.__str__()}")
|
||||
|
||||
def result_reporter(self, result:dict|None=None):
|
||||
# def result_reporter(self, result:TypedDict[]|None=None):
|
||||
"""
|
||||
Report any anomolous results - if any - to the user
|
||||
|
||||
@@ -158,6 +165,8 @@ class App(QMainWindow):
|
||||
if result != None:
|
||||
msg = AlertPop(message=result['message'], status=result['status'])
|
||||
msg.exec()
|
||||
else:
|
||||
self.statusBar().showMessage("Action completed sucessfully.", 5000)
|
||||
|
||||
def importSubmission(self):
|
||||
"""
|
||||
@@ -211,13 +220,15 @@ class App(QMainWindow):
|
||||
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 = 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)
|
||||
# send reagent to db
|
||||
# store_reagent(ctx=self.ctx, reagent=reagent)
|
||||
result = store_object(ctx=self.ctx, object=reagent)
|
||||
self.result_reporter(result=result)
|
||||
return reagent
|
||||
|
||||
def generateReport(self):
|
||||
@@ -263,6 +274,7 @@ class App(QMainWindow):
|
||||
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)
|
||||
@@ -327,7 +339,7 @@ class AddSubForm(QWidget):
|
||||
self.tabs.addTab(self.tab2,"Controls")
|
||||
self.tabs.addTab(self.tab3, "Add Kit")
|
||||
# Create submission adder form
|
||||
self.formwidget = QWidget(self)
|
||||
self.formwidget = SubmissionFormWidget(self)
|
||||
self.formlayout = QVBoxLayout(self)
|
||||
self.formwidget.setLayout(self.formlayout)
|
||||
self.formwidget.setFixedWidth(300)
|
||||
@@ -381,3 +393,32 @@ class AddSubForm(QWidget):
|
||||
self.layout.addWidget(self.tabs)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
logger.debug(f"Setting form widget...")
|
||||
super().__init__(parent)
|
||||
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
"qt_scrollarea_vcontainer", "submit_btn"
|
||||
]
|
||||
|
||||
def parse_form(self) -> Tuple[dict, list]:
|
||||
logger.debug(f"Hello from parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
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)}")
|
||||
match widget:
|
||||
case ImportReagent():
|
||||
reagents.append(dict(name=widget.objectName().replace("lot_", ""), lot=widget.currentText()))
|
||||
case QLineEdit():
|
||||
info[widget.objectName()] = widget.text()
|
||||
case QComboBox():
|
||||
info[widget.objectName()] = widget.currentText()
|
||||
case QDateEdit():
|
||||
info[widget.objectName()] = widget.date().toPyDate()
|
||||
logger.debug(f"Info: {pformat(info)}")
|
||||
logger.debug(f"Reagents: {pformat(reagents)}")
|
||||
return info, reagents
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:
|
||||
def extract_form_info(object) -> dict:
|
||||
"""
|
||||
retrieves object names and values from form
|
||||
DEPRECIATED. Replaced by individual form parser methods.
|
||||
|
||||
Args:
|
||||
object (_type_): the form widget
|
||||
@@ -64,7 +65,7 @@ def extract_form_info(object) -> dict:
|
||||
|
||||
from frontend.custom_widgets import ReagentTypeForm
|
||||
dicto = {}
|
||||
reagents = {}
|
||||
reagents = []
|
||||
logger.debug(f"Object type: {type(object)}")
|
||||
# grab all widgets in form
|
||||
try:
|
||||
@@ -85,8 +86,17 @@ def extract_form_info(object) -> dict:
|
||||
case ReagentTypeForm():
|
||||
reagent = extract_form_info(item)
|
||||
logger.debug(f"Reagent found: {reagent}")
|
||||
reagents[reagent["name"].strip()] = {'eol_ext':int(reagent['eol'])}
|
||||
if isinstance(reagent, tuple):
|
||||
reagent = reagent[0]
|
||||
# reagents[reagent["name"].strip()] = {'eol':int(reagent['eol'])}
|
||||
reagents.append({k:v for k,v in reagent.items() if k not in ['', 'qt_spinbox_lineedit']})
|
||||
# value for ad hoc check above
|
||||
if isinstance(dicto, tuple):
|
||||
logger.warning(f"Got tuple for dicto for some reason.")
|
||||
dicto = dicto[0]
|
||||
if isinstance(reagents, tuple):
|
||||
logger.warning(f"Got tuple for reagents for some reason.")
|
||||
reagents = reagents[0]
|
||||
if reagents != {}:
|
||||
return dicto, reagents
|
||||
return dicto
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
Contains miscellaneous widgets for frontend functions
|
||||
'''
|
||||
from datetime import date
|
||||
from pprint import pformat
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout,
|
||||
QLineEdit, QComboBox, QDialog,
|
||||
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
|
||||
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
|
||||
QHBoxLayout
|
||||
QHBoxLayout, QScrollArea
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QDate, QSize
|
||||
from tools import check_not_nan, jinja_template_loading, Settings
|
||||
from ..all_window_functions import extract_form_info
|
||||
from backend.db import construct_kit_from_yaml, \
|
||||
from backend.db.functions import construct_kit_from_yaml, \
|
||||
lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association
|
||||
from backend.db.models import SubmissionTypeKitTypeAssociation
|
||||
from sqlalchemy import FLOAT, INTEGER, String
|
||||
import logging
|
||||
import numpy as np
|
||||
from .pop_ups import AlertPop
|
||||
from backend.pydant import PydReagent
|
||||
from typing import Tuple
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -84,6 +87,12 @@ class AddReagentForm(QDialog):
|
||||
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
|
||||
@@ -121,6 +130,9 @@ class ReportDatePicker(QDialog):
|
||||
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
|
||||
@@ -128,8 +140,14 @@ class KitAdder(QWidget):
|
||||
def __init__(self, parent_ctx:Settings) -> 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)
|
||||
# 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)
|
||||
@@ -138,42 +156,65 @@ class KitAdder(QWidget):
|
||||
kit_name = QLineEdit()
|
||||
kit_name.setObjectName("kit_name")
|
||||
self.grid.addWidget(kit_name,2,1)
|
||||
self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
|
||||
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(lookup_all_sample_types(ctx=parent_ctx))
|
||||
used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
|
||||
used_for.setEditable(True)
|
||||
self.grid.addWidget(used_for,3,1)
|
||||
# set cost per run
|
||||
self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
|
||||
# widget to get constant cost
|
||||
const_cost = QDoubleSpinBox() #QSpinBox()
|
||||
const_cost.setObjectName("const_cost")
|
||||
const_cost.setMinimum(0)
|
||||
const_cost.setMaximum(9999)
|
||||
self.grid.addWidget(const_cost,4,1)
|
||||
self.grid.addWidget(QLabel("Cost per column (multidrop reagents, etc.):"),5,0)
|
||||
# widget to get mutable costs per column
|
||||
mut_cost_col = QDoubleSpinBox() #QSpinBox()
|
||||
mut_cost_col.setObjectName("mut_cost_col")
|
||||
mut_cost_col.setMinimum(0)
|
||||
mut_cost_col.setMaximum(9999)
|
||||
self.grid.addWidget(mut_cost_col,5,1)
|
||||
self.grid.addWidget(QLabel("Cost per sample (tips, reagents, etc.):"),6,0)
|
||||
# widget to get mutable costs per column
|
||||
mut_cost_samp = QDoubleSpinBox() #QSpinBox()
|
||||
mut_cost_samp.setObjectName("mut_cost_samp")
|
||||
mut_cost_samp.setMinimum(0)
|
||||
mut_cost_samp.setMaximum(9999)
|
||||
self.grid.addWidget(mut_cost_samp,6,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.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
|
||||
# # widget to get constant cost
|
||||
# const_cost = QDoubleSpinBox() #QSpinBox()
|
||||
# const_cost.setObjectName("const_cost")
|
||||
# const_cost.setMinimum(0)
|
||||
# const_cost.setMaximum(9999)
|
||||
# self.grid.addWidget(const_cost,4,1)
|
||||
# self.grid.addWidget(QLabel("Cost per column (multidrop reagents, etc.):"),5,0)
|
||||
# # widget to get mutable costs per column
|
||||
# mut_cost_col = QDoubleSpinBox() #QSpinBox()
|
||||
# mut_cost_col.setObjectName("mut_cost_col")
|
||||
# mut_cost_col.setMinimum(0)
|
||||
# mut_cost_col.setMaximum(9999)
|
||||
# self.grid.addWidget(mut_cost_col,5,1)
|
||||
# self.grid.addWidget(QLabel("Cost per sample (tips, reagents, etc.):"),6,0)
|
||||
# # widget to get mutable costs per column
|
||||
# mut_cost_samp = QDoubleSpinBox() #QSpinBox()
|
||||
# mut_cost_samp.setObjectName("mut_cost_samp")
|
||||
# mut_cost_samp.setMinimum(0)
|
||||
# mut_cost_samp.setMaximum(9999)
|
||||
# self.grid.addWidget(mut_cost_samp,6,1)
|
||||
# button to add additional reagent types
|
||||
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:
|
||||
"""
|
||||
@@ -181,9 +222,11 @@ class KitAdder(QWidget):
|
||||
"""
|
||||
# get bottommost row
|
||||
maxrow = self.grid.rowCount()
|
||||
reg_form = ReagentTypeForm(parent_ctx=self.ctx)
|
||||
reg_form = ReagentTypeForm(ctx=self.ctx)
|
||||
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||
self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||
self.grid.addWidget(reg_form, maxrow,0,1,4)
|
||||
|
||||
|
||||
|
||||
def submit(self) -> None:
|
||||
@@ -191,40 +234,62 @@ class KitAdder(QWidget):
|
||||
send kit to database
|
||||
"""
|
||||
# get form info
|
||||
info, reagents = extract_form_info(self)
|
||||
logger.debug(f"kit info: {info}")
|
||||
yml_type = {}
|
||||
try:
|
||||
yml_type['password'] = info['password']
|
||||
except KeyError:
|
||||
pass
|
||||
used = info['used_for']
|
||||
yml_type[used] = {}
|
||||
yml_type[used]['kits'] = {}
|
||||
yml_type[used]['kits'][info['kit_name']] = {}
|
||||
yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["const_cost"]
|
||||
yml_type[used]['kits'][info['kit_name']]['mutable_cost_column'] = info["mut_cost_col"]
|
||||
yml_type[used]['kits'][info['kit_name']]['mutable_cost_sample'] = info["mut_cost_samp"]
|
||||
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
|
||||
logger.debug(yml_type)
|
||||
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
|
||||
# for reagent in reagents:
|
||||
# new_dict = {}
|
||||
# for k,v in reagent.items():
|
||||
# if "_" in k:
|
||||
# key, sub_key = k.split("_")
|
||||
# if key not in new_dict.keys():
|
||||
# new_dict[key] = {}
|
||||
# logger.debug(f"Adding key {key}, {sub_key} and value {v} to {new_dict}")
|
||||
# new_dict[key][sub_key] = v
|
||||
# else:
|
||||
# new_dict[k] = v
|
||||
# info['reagent_types'].append(new_dict)
|
||||
logger.debug(pformat(info))
|
||||
# send to kit constructor
|
||||
result = construct_kit_from_yaml(ctx=self.ctx, exp=yml_type)
|
||||
result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
|
||||
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, ctx:dict) -> None:
|
||||
def __init__(self, ctx:Settings) -> None:
|
||||
super().__init__()
|
||||
grid = QGridLayout()
|
||||
self.setLayout(grid)
|
||||
grid.addWidget(QLabel("Name (*Exactly* as it appears in the excel submission form):"),0,0)
|
||||
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||
# Widget to get reagent info
|
||||
self.reagent_getter = QComboBox()
|
||||
self.reagent_getter.setObjectName("name")
|
||||
self.reagent_getter.setObjectName("rtname")
|
||||
# lookup all reagent type names from db
|
||||
lookup = lookup_reagent_types(ctx=ctx)
|
||||
logger.debug(f"Looked up ReagentType names: {lookup}")
|
||||
@@ -233,10 +298,66 @@ class ReagentTypeForm(QWidget):
|
||||
grid.addWidget(self.reagent_getter,0,1)
|
||||
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
|
||||
# widget to get extension of life
|
||||
eol = QSpinBox()
|
||||
eol.setObjectName('eol')
|
||||
eol.setMinimum(0)
|
||||
grid.addWidget(eol, 0,3)
|
||||
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):
|
||||
"""
|
||||
@@ -336,3 +457,4 @@ class ParsedQLabel(QLabel):
|
||||
self.setText(f"Parsed {output}")
|
||||
else:
|
||||
self.setText(f"MISSING {output}")
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from PyQt6.QtWidgets import (
|
||||
from tools import jinja_template_loading
|
||||
import logging
|
||||
from backend.db.functions import lookup_kit_types, lookup_submission_type
|
||||
from typing import Literal
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -36,7 +37,7 @@ class AlertPop(QMessageBox):
|
||||
"""
|
||||
Dialog to show an alert.
|
||||
"""
|
||||
def __init__(self, message:str, status:str) -> QMessageBox:
|
||||
def __init__(self, message:str, status:Literal['information', 'question', 'warning', 'critical']) -> QMessageBox:
|
||||
super().__init__()
|
||||
# select icon by string
|
||||
icon = getattr(QMessageBox.Icon, status.title())
|
||||
|
||||
@@ -23,7 +23,7 @@ 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
|
||||
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
|
||||
from getpass import getuser
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -265,18 +265,19 @@ class SubmissionDetails(QDialog):
|
||||
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...")
|
||||
plate_dicto = sub.hitpick_plate()
|
||||
self.plate_dicto = sub.hitpick_plate()
|
||||
logger.debug(f"Making platemap...")
|
||||
platemap = make_plate_map(plate_dicto)
|
||||
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')
|
||||
template = env.get_template("submission_details.html")
|
||||
self.html = template.render(sub=self.base_dict)
|
||||
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)
|
||||
@@ -290,6 +291,8 @@ class SubmissionDetails(QDialog):
|
||||
btn.setParent(self)
|
||||
btn.setFixedWidth(900)
|
||||
btn.clicked.connect(self.export)
|
||||
with open("test.html", "w") as f:
|
||||
f.write(self.html)
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
@@ -303,9 +306,18 @@ class SubmissionDetails(QDialog):
|
||||
if fname.__str__() == ".":
|
||||
logger.debug("Saving pdf was cancelled.")
|
||||
return
|
||||
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.html, dest=f)
|
||||
pisa.CreatePDF(self.html2, dest=f)
|
||||
except PermissionError as e:
|
||||
logger.error(f"Error saving pdf: {e}")
|
||||
msg = QMessageBox()
|
||||
|
||||
@@ -6,6 +6,7 @@ import difflib
|
||||
from getpass import getuser
|
||||
import inspect
|
||||
import pprint
|
||||
import re
|
||||
import yaml
|
||||
import json
|
||||
from typing import Tuple, List
|
||||
@@ -19,12 +20,12 @@ from PyQt6.QtWidgets import (
|
||||
QMainWindow, QLabel, QWidget, QPushButton,
|
||||
QLineEdit, QComboBox, QDateEdit
|
||||
)
|
||||
from .all_window_functions import extract_form_info, select_open_file, select_save_file
|
||||
from .all_window_functions import select_open_file, select_save_file
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend.db.functions import (
|
||||
construct_submission_info, lookup_reagents, construct_kit_from_yaml, construct_org_from_yaml, get_control_subtypes,
|
||||
update_subsampassoc_with_pcr, check_kit_integrity, update_last_used, lookup_organizations, lookup_kit_types,
|
||||
lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object
|
||||
lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object, lookup_submission_type
|
||||
)
|
||||
from backend.excel.parser import SheetParser, PCRParser, SampleParser
|
||||
from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df
|
||||
@@ -139,11 +140,22 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
|
||||
logger.debug(f"{field}:\n\t{value}")
|
||||
obj.samples = value
|
||||
continue
|
||||
case 'submission_category':
|
||||
add_widget = QComboBox()
|
||||
cats = ['Diagnostic', "Surveillance", "Research"]
|
||||
cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
|
||||
try:
|
||||
cats.insert(0, cats.pop(cats.index(value['value'])))
|
||||
except ValueError:
|
||||
cats.insert(0, cats.pop(cats.index(pyd.submission_type['value'])))
|
||||
add_widget.addItems(cats)
|
||||
case "ctx":
|
||||
continue
|
||||
case 'reagents':
|
||||
# NOTE: This is now set to run when the extraction kit is updated.
|
||||
continue
|
||||
case 'csv':
|
||||
continue
|
||||
case _:
|
||||
# anything else gets added in as a line edit
|
||||
add_widget = QLineEdit()
|
||||
@@ -166,6 +178,7 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
|
||||
if "csv" in pyd.model_extra:
|
||||
obj.csv = pyd.model_extra['csv']
|
||||
logger.debug(f"All attributes of obj:\n{pprint.pformat(obj.__dict__)}")
|
||||
|
||||
return obj, result
|
||||
|
||||
def kit_reload_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
@@ -208,7 +221,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
||||
# get current kit being used
|
||||
obj.ext_kit = kit_widget.currentText()
|
||||
for item in obj.reagents:
|
||||
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False, label_name=f"lot_{item.type}_label"))
|
||||
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False))
|
||||
reagent = dict(type=item.type, lot=item.lot, exp=item.exp, name=item.name)
|
||||
add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit)
|
||||
obj.table_widget.formlayout.addWidget(add_widget)
|
||||
@@ -218,7 +231,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
||||
result = dict(message=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in obj.missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning")
|
||||
for item in obj.missing_reagents:
|
||||
# Add label that has parsed as False to show "MISSING" label.
|
||||
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False, label_name=f"missing_{item.type}_label"))
|
||||
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False))
|
||||
# Set default parameters for the empty reagent.
|
||||
reagent = dict(type=item.type, lot=None, exp=date.today(), name=None)
|
||||
# create and add widget
|
||||
@@ -227,7 +240,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
||||
obj.table_widget.formlayout.addWidget(add_widget)
|
||||
# Add submit button to the form.
|
||||
submit_btn = QPushButton("Submit")
|
||||
submit_btn.setObjectName("lot_submit_btn")
|
||||
submit_btn.setObjectName("submit_btn")
|
||||
obj.table_widget.formlayout.addWidget(submit_btn)
|
||||
submit_btn.clicked.connect(obj.submit_new_sample)
|
||||
return obj, result
|
||||
@@ -245,32 +258,37 @@ def submit_new_sample_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
logger.debug(f"\n\nBeginning Submission\n\n")
|
||||
result = None
|
||||
# extract info from the form widgets
|
||||
info = extract_form_info(obj.table_widget.tab1)
|
||||
# seperate out reagents
|
||||
reagents = {k.replace("lot_", ""):v for k,v in info.items() if k.startswith("lot_")}
|
||||
info = {k:v for k,v in info.items() if not k.startswith("lot_")}
|
||||
# info = extract_form_info(obj.table_widget.tab1)
|
||||
# if isinstance(info, tuple):
|
||||
# logger.warning(f"Got tuple for info for some reason.")
|
||||
# info = info[0]
|
||||
# # seperate out reagents
|
||||
# reagents = {k.replace("lot_", ""):v for k,v in info.items() if k.startswith("lot_")}
|
||||
# info = {k:v for k,v in info.items() if not k.startswith("lot_")}
|
||||
info, reagents = obj.table_widget.formwidget.parse_form()
|
||||
logger.debug(f"Info: {info}")
|
||||
logger.debug(f"Reagents: {reagents}")
|
||||
parsed_reagents = []
|
||||
# compare reagents in form to reagent database
|
||||
for reagent in reagents:
|
||||
# Lookup any existing reagent of this type with this lot number
|
||||
wanted_reagent = lookup_reagents(ctx=obj.ctx, lot_number=reagents[reagent], reagent_type=reagent)
|
||||
wanted_reagent = lookup_reagents(ctx=obj.ctx, lot_number=reagent['lot'], reagent_type=reagent['name'])
|
||||
logger.debug(f"Looked up reagent: {wanted_reagent}")
|
||||
# if reagent not found offer to add to database
|
||||
if wanted_reagent == None:
|
||||
r_lot = reagents[reagent]
|
||||
dlg = QuestionAsker(title=f"Add {r_lot}?", message=f"Couldn't find reagent type {reagent.strip('Lot')}: {r_lot} in the database.\n\nWould you like to add it?")
|
||||
# r_lot = reagent[reagent]
|
||||
r_lot = reagent['lot']
|
||||
dlg = QuestionAsker(title=f"Add {r_lot}?", message=f"Couldn't find reagent type {reagent['name'].strip('Lot')}: {r_lot} in the database.\n\nWould you like to add it?")
|
||||
if dlg.exec():
|
||||
logger.debug(f"Looking through {pprint.pformat(obj.reagents)} for reagent {reagent}")
|
||||
logger.debug(f"Looking through {pprint.pformat(obj.reagents)} for reagent {reagent['name']}")
|
||||
try:
|
||||
picked_reagent = [item for item in obj.reagents if item.type == reagent][0]
|
||||
picked_reagent = [item for item in obj.reagents if item.type == reagent['name']][0]
|
||||
except IndexError:
|
||||
logger.error(f"Couldn't find {reagent} in obj.reagents. Checking missing reagents {pprint.pformat(obj.missing_reagents)}")
|
||||
picked_reagent = [item for item in obj.missing_reagents if item.type == reagent][0]
|
||||
logger.debug(f"checking reagent: {reagent} in obj.reagents. Result: {picked_reagent}")
|
||||
logger.error(f"Couldn't find {reagent['name']} in obj.reagents. Checking missing reagents {pprint.pformat(obj.missing_reagents)}")
|
||||
picked_reagent = [item for item in obj.missing_reagents if item.type == reagent['name']][0]
|
||||
logger.debug(f"checking reagent: {reagent['name']} in obj.reagents. Result: {picked_reagent}")
|
||||
expiry_date = picked_reagent.exp
|
||||
wanted_reagent = obj.add_reagent(reagent_lot=r_lot, reagent_type=reagent.replace("lot_", ""), expiry=expiry_date, name=picked_reagent.name)
|
||||
wanted_reagent = obj.add_reagent(reagent_lot=r_lot, reagent_type=reagent['name'].replace("lot_", ""), expiry=expiry_date, name=picked_reagent.name)
|
||||
else:
|
||||
# In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||
logger.debug("Will not add reagent.")
|
||||
@@ -348,14 +366,13 @@ def generate_report_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
# ask for date ranges
|
||||
dlg = ReportDatePicker()
|
||||
if dlg.exec():
|
||||
info = extract_form_info(dlg)
|
||||
# info = extract_form_info(dlg)
|
||||
info = dlg.parse_form()
|
||||
logger.debug(f"Report info: {info}")
|
||||
# find submissions based on date range
|
||||
# subs = lookup_submissions_by_date_range(ctx=obj.ctx, start_date=info['start_date'], end_date=info['end_date'])
|
||||
subs = lookup_submissions(ctx=obj.ctx, start_date=info['start_date'], end_date=info['end_date'])
|
||||
# convert each object to dict
|
||||
records = [item.report_dict() for item in subs]
|
||||
@@ -542,6 +559,7 @@ def chart_maker_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
def link_controls_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Link scraped controls to imported submissions.
|
||||
NOTE: Depreciated due to improvements in controls scraper.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -624,6 +642,8 @@ def link_extractions_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
# 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'])
|
||||
# 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
|
||||
@@ -687,6 +707,8 @@ def link_pcr_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
# 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'])
|
||||
# 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:
|
||||
@@ -838,11 +860,14 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
|
||||
new_info = []
|
||||
logger.debug(f"Parsing from relevant info map: {pprint.pformat(relevant_info_map)}")
|
||||
for item in relevant_info:
|
||||
new_item = {}
|
||||
new_item['type'] = item
|
||||
new_item['location'] = relevant_info_map[item]
|
||||
new_item['value'] = relevant_info[item]
|
||||
new_info.append(new_item)
|
||||
try:
|
||||
new_item = {}
|
||||
new_item['type'] = item
|
||||
new_item['location'] = relevant_info_map[item]
|
||||
new_item['value'] = relevant_info[item]
|
||||
new_info.append(new_item)
|
||||
except KeyError:
|
||||
logger.error(f"Unable to fill in {item}, not found in relevant info.")
|
||||
logger.debug(f"New reagents: {new_reagents}")
|
||||
logger.debug(f"New info: {new_info}")
|
||||
# open a new workbook using openpyxl
|
||||
@@ -888,17 +913,16 @@ def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]
|
||||
"""
|
||||
def get_plates(input_sample_number:str, plates:list) -> Tuple[int, str]:
|
||||
logger.debug(f"Looking up {input_sample_number} in {plates}")
|
||||
# samp = lookup_ww_sample_by_processing_number(ctx=obj.ctx, processing_number=input_sample_number)
|
||||
samp = lookup_samples(ctx=obj.ctx, ww_processing_num=input_sample_number)
|
||||
if samp == None:
|
||||
# samp = lookup_sample_by_submitter_id(ctx=obj.ctx, submitter_id=input_sample_number)
|
||||
samp = lookup_samples(ctx=obj.ctx, submitter_id=input_sample_number)
|
||||
if samp == None:
|
||||
return None, None
|
||||
logger.debug(f"Got sample: {samp}")
|
||||
# new_plates = [(iii+1, lookup_sub_samp_association_by_plate_sample(ctx=obj.ctx, rsl_sample_num=samp, rsl_plate_num=lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=plate))) for iii, plate in enumerate(plates)]
|
||||
new_plates = [(iii+1, lookup_submission_sample_association(ctx=obj.ctx, sample=samp, submission=plate)) for iii, plate in enumerate(plates)]
|
||||
logger.debug(f"Associations: {pprint.pformat(new_plates)}")
|
||||
try:
|
||||
plate_num, plate = next(assoc for assoc in new_plates if assoc[1] is not None)
|
||||
plate_num, plate = next(assoc for assoc in new_plates if assoc[1])
|
||||
except StopIteration:
|
||||
plate_num, plate = None, None
|
||||
logger.debug(f"Plate number {plate_num} is {plate}")
|
||||
@@ -907,11 +931,19 @@ def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]
|
||||
xl = pd.ExcelFile(fname)
|
||||
sprsr = SampleParser(ctx=obj.ctx, xl=xl, submission_type="First Strand")
|
||||
_, samples = sprsr.parse_samples(generate=False)
|
||||
logger.debug(f"Samples: {pformat(samples)}")
|
||||
logger.debug("Called first strand sample parser")
|
||||
plates = sprsr.grab_plates()
|
||||
logger.debug(f"Plates: {pformat(plates)}")
|
||||
output_samples = []
|
||||
logger.debug(f"Samples: {pprint.pformat(samples)}")
|
||||
logger.debug(f"Samples: {pformat(samples)}")
|
||||
old_plate_number = 1
|
||||
for item in samples:
|
||||
try:
|
||||
item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0]
|
||||
except AttributeError:
|
||||
item['well'] = item
|
||||
item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip()
|
||||
new_dict = {}
|
||||
new_dict['sample'] = item['submitter_id']
|
||||
if item['submitter_id'] == "NTC1":
|
||||
@@ -967,6 +999,7 @@ def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, d
|
||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||
obj.reagents = []
|
||||
obj.missing_reagents = []
|
||||
# Remove previous reagent widgets
|
||||
[item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget) if item.objectName().startswith("lot_") or item.objectName().startswith("missing_")]
|
||||
reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
|
||||
logger.debug(f"Got reagents: {reagents}")
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
import sys
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import numpy as np
|
||||
from tools import check_if_app
|
||||
from tools import check_if_app, jinja_template_loading
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -81,4 +81,31 @@ def make_plate_map(sample_list:list) -> Image:
|
||||
letter = row_dict[num-1]
|
||||
y = (num * 100) - 10
|
||||
draw.text((10, y), letter, (0,0,0),font=font)
|
||||
return new_img
|
||||
return new_img
|
||||
|
||||
def make_plate_map_html(sample_list:list, plate_rows:int=8, plate_columns=12) -> str:
|
||||
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
|
||||
for sample in sample_list:
|
||||
if sample['positive']:
|
||||
sample['background_color'] = "#f10f07"
|
||||
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
|
||||
Reference in New Issue
Block a user