Commit pre-refactor for code cleanup.

This commit is contained in:
Landon Wark
2023-02-10 10:22:39 -06:00
parent c3db706e7c
commit a9ce9514fc
11 changed files with 400 additions and 272 deletions

View File

@@ -1,4 +1,4 @@
# __init__.py # __init__.py
# Version of the realpython-reader package # Version of the realpython-reader package
__version__ = "1.2.2" __version__ = "1.2.3"

View File

@@ -329,7 +329,7 @@ def lookup_all_submissions_by_type(ctx:dict, type:str|None=None) -> list[models.
if type == None: if type == None:
subs = ctx['database_session'].query(models.BasicSubmission).all() subs = ctx['database_session'].query(models.BasicSubmission).all()
else: else:
subs = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submission_type==type).all() subs = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submission_type==type.lower().replace(" ", "_")).all()
return subs return subs
def lookup_all_orgs(ctx:dict) -> list[models.Organization]: def lookup_all_orgs(ctx:dict) -> list[models.Organization]:

View File

@@ -0,0 +1,21 @@
from ..models import *
import logging
logger = logging.getLogger(f"submissions.{__name__}")
def check_kit_integrity(sub:BasicSubmission):
ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types]
logger.debug(f"Kit reagents: {ext_kit_rtypes}")
reagenttypes = [reagent.type.name for reagent in sub.reagents]
logger.debug(f"Submission reagents: {reagenttypes}")
check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}")
common = list(set(ext_kit_rtypes).intersection(reagenttypes))
logger.debug(f"common reagents types: {common}")
if check:
result = None
else:
result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[x.upper for x in ext_kit_rtypes if x not in common]}\n\nAlternatively, you may have set the wrong extraction kit."}
return result

View File

@@ -78,7 +78,7 @@ class Reagent(Base):
Returns: Returns:
str: string representing this object's lot number str: string representing this object's lot number
""" """
return self.lot return str(self.lot)
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
""" """

View File

@@ -7,6 +7,7 @@ import re
import numpy as np import numpy as np
from datetime import date from datetime import date
import uuid import uuid
from frontend.functions import check_not_nan
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -59,7 +60,8 @@ class SheetParser(object):
def _parse_unknown(self) -> None: def _parse_unknown(self) -> None:
""" """
Dummy function to handle unknown excel structures Dummy function to handle unknown excel structures
""" """
logger.error(f"Unknown excel workbook structure. Cannot parse.")
self.sub = None self.sub = None
@@ -96,11 +98,11 @@ class SheetParser(object):
if ii == 11: if ii == 11:
continue continue
logger.debug(f"Running reagent parse for {row[1]} with type {type(row[1])} and value: {row[2]} with type {type(row[2])}") logger.debug(f"Running reagent parse for {row[1]} with type {type(row[1])} and value: {row[2]} with type {type(row[2])}")
try: # try:
check = not np.isnan(row[1]) # check = not np.isnan(row[1])
except TypeError: # except TypeError:
check = True # check = True
if not isinstance(row[2], float) and check: if not isinstance(row[2], float) and check_not_nan(row[1]):
# must be prefixed with 'lot_' to be recognized by gui # must be prefixed with 'lot_' to be recognized by gui
try: try:
reagent_type = row[1].replace(' ', '_').lower().strip() reagent_type = row[1].replace(' ', '_').lower().strip()
@@ -114,7 +116,18 @@ class SheetParser(object):
logger.debug(f"Couldn't upperize {row[2]}, must be a number") logger.debug(f"Couldn't upperize {row[2]}, must be a number")
output_var = row[2] output_var = row[2]
logger.debug(f"Output variable is {output_var}") logger.debug(f"Output variable is {output_var}")
self.sub[f"lot_{reagent_type}"] = output_var # self.sub[f"lot_{reagent_type}"] = output_var
# update 2023-02-10 to above allowing generation of expiry date in adding reagent to db.
logger.debug(f"Expiry date for imported reagent: {row[3]}")
try:
check = not np.isnan(row[3])
except TypeError:
check = True
if check:
expiry = row[3].date()
else:
expiry = date.today()
self.sub[f"lot_{reagent_type}"] = {'lot':output_var, 'exp':expiry}
submission_info = self._parse_generic("Sample List") submission_info = self._parse_generic("Sample List")
# iloc is [row][column] and the first row is set as header row so -2 # iloc is [row][column] and the first row is set as header row so -2

View File

@@ -33,7 +33,9 @@ def make_report_xlsx(records:list[dict]) -> DataFrame:
# put submissions with the same lab together # put submissions with the same lab together
df = df.sort_values("Submitting Lab") df = df.sort_values("Submitting Lab")
# aggregate cost and sample count columns # aggregate cost and sample count columns
df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Cost': ['sum', 'count'], 'Sample Count':['sum']}) df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Extraction Kit':'count', 'Cost': 'sum', 'Sample Count':'sum'})
df2 = df2.rename(columns={"Extraction Kit": 'Kit Count'})
logger.debug(f"Output daftaframe for xlsx: {df2.columns}")
# apply formating to cost column # apply formating to cost column
# df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format) # df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format)
return df2 return df2
@@ -54,11 +56,14 @@ def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str:
""" """
old_lab = "" old_lab = ""
output = [] output = []
logger.debug(f"Report DataFrame: {df}")
for ii, row in enumerate(df.iterrows()): for ii, row in enumerate(df.iterrows()):
row = [item for item in row] row = [item for item in row]
logger.debug(f"Row: {row}")
lab = row[0][0] lab = row[0][0]
logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") logger.debug(f"Old lab: {old_lab}, Current lab: {lab}")
kit = dict(name=row[0][1], cost=row[1][('Cost', 'sum')], plate_count=int(row[1][('Cost', 'count')]), sample_count=int(row[1][('Sample Count', 'sum')])) kit = dict(name=row[0][1], cost=row[1]['Cost'], plate_count=int(row[1]['Kit Count']), sample_count=int(row[1]['Sample Count']))
if lab == old_lab: if lab == old_lab:
output[ii-1]['kits'].append(kit) output[ii-1]['kits'].append(kit)
output[ii-1]['total_cost'] += kit['cost'] output[ii-1]['total_cost'] += kit['cost']

View File

@@ -10,9 +10,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
# import pandas as pd # import pandas as pd
from pathlib import Path from pathlib import Path
import plotly import plotly
import pandas as pd import pandas as pd
@@ -21,7 +19,7 @@ from xhtml2pdf import pisa
# import plotly.express as px # import plotly.express as px
import yaml import yaml
import pprint import pprint
import numpy as np
from backend.excel.parser import SheetParser from backend.excel.parser import SheetParser
from backend.excel.reports import convert_control_by_mode, convert_data_list_to_df from backend.excel.reports import convert_control_by_mode, convert_data_list_to_df
from backend.db import (construct_submission_info, lookup_reagent, from backend.db import (construct_submission_info, lookup_reagent,
@@ -30,9 +28,13 @@ from backend.db import (construct_submission_info, lookup_reagent,
get_all_Control_Types_names, create_kit_from_yaml, get_all_available_modes, get_all_controls_by_type, get_all_Control_Types_names, create_kit_from_yaml, get_all_available_modes, get_all_controls_by_type,
get_control_subtypes, lookup_all_submissions_by_type, get_all_controls, lookup_submission_by_rsl_num get_control_subtypes, lookup_all_submissions_by_type, get_all_controls, lookup_submission_by_rsl_num
) )
from .functions import check_kit_integrity, check_not_nan
from backend.excel.reports import make_report_xlsx, make_report_html from backend.excel.reports import make_report_xlsx, make_report_html
import numpy import numpy
from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion from frontend.custom_widgets.sub_details import SubmissionsSheet
from frontend.custom_widgets.pop_ups import AddReagentQuestion, OverwriteSubQuestion, AlertPop
from frontend.custom_widgets import AddReagentForm, ReportDatePicker, KitAdder, ControlsDatePicker
import logging import logging
import difflib import difflib
from getpass import getuser from getpass import getuser
@@ -130,6 +132,7 @@ class App(QMainWindow):
logger.debug(self.ctx) logger.debug(self.ctx)
# initialize samples # initialize samples
self.samples = [] self.samples = []
self.reagents = {}
# set file dialog # set file dialog
home_dir = str(Path(self.ctx["directory_path"])) home_dir = str(Path(self.ctx["directory_path"]))
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0]) fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0])
@@ -180,14 +183,9 @@ class App(QMainWindow):
# create label # create label
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# if extraction kit not available, all other values fail # if extraction kit not available, all other values fail
if prsr.sub[item] == 'nan': if np.isnan(prsr.sub[item]):
msg = QMessageBox() msg = AlertPop(message="Make sure to check your extraction kit!", status="warning")
# msg.setIcon(QMessageBox.critical)
msg.setText("Error")
msg.setInformativeText('You need to enter a value for extraction kit.')
msg.setWindowTitle("Error")
msg.exec() msg.exec()
break
# create combobox to hold looked up kits # create combobox to hold looked up kits
add_widget = QComboBox() add_widget = QComboBox()
# lookup existing kits by 'submission_type' decided on by sheetparser # lookup existing kits by 'submission_type' decided on by sheetparser
@@ -216,13 +214,13 @@ class App(QMainWindow):
query_var = item.replace("lot_", "") query_var = item.replace("lot_", "")
logger.debug(f"Query for: {query_var}") logger.debug(f"Query for: {query_var}")
if isinstance(prsr.sub[item], numpy.float64): if isinstance(prsr.sub[item], numpy.float64):
logger.debug(f"{prsr.sub[item]} is a numpy float!") logger.debug(f"{prsr.sub[item]['lot']} is a numpy float!")
try: try:
prsr.sub[item] = int(prsr.sub[item]) prsr.sub[item] = int(prsr.sub[item]['lot'])
except ValueError: except ValueError:
pass pass
# query for reagents using type name from sheet and kit from sheet # query for reagents using type name from sheet and kit from sheet
logger.debug(f"Attempting lookup of reagents by type: {query_var} and kit: {prsr.sub['extraction_kit']}") logger.debug(f"Attempting lookup of reagents by type: {query_var}")
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work. # below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name(ctx=self.ctx, type_name=query_var)]#, kit_name=prsr.sub['extraction_kit'])] relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name(ctx=self.ctx, type_name=query_var)]#, kit_name=prsr.sub['extraction_kit'])]
output_reg = [] output_reg = []
@@ -235,15 +233,12 @@ class App(QMainWindow):
relevant_reagents = output_reg relevant_reagents = output_reg
logger.debug(f"Relevant reagents: {relevant_reagents}") logger.debug(f"Relevant reagents: {relevant_reagents}")
# if reagent in sheet is not found insert it into items # if reagent in sheet is not found insert it into items
if prsr.sub[item] not in relevant_reagents and prsr.sub[item] != 'nan': if str(prsr.sub[item]['lot']) not in relevant_reagents and prsr.sub[item]['lot'] != 'nan':
try: if check_not_nan(prsr.sub[item]['lot']):
check = not numpy.isnan(prsr.sub[item]) relevant_reagents.insert(0, str(prsr.sub[item]['lot']))
except TypeError: logger.debug(f"New relevant reagents: {relevant_reagents}")
check = True
if check:
relevant_reagents.insert(0, str(prsr.sub[item]))
logger.debug(f"Relevant reagents: {relevant_reagents}")
add_widget.addItems(relevant_reagents) add_widget.addItems(relevant_reagents)
self.reagents[item] = prsr.sub[item]
# TODO: make samples not appear in frame. # TODO: make samples not appear in frame.
case 'samples': case 'samples':
# hold samples in 'self' until form submitted # hold samples in 'self' until form submitted
@@ -259,6 +254,7 @@ class App(QMainWindow):
submit_btn = QPushButton("Submit") submit_btn = QPushButton("Submit")
self.table_widget.formlayout.addWidget(submit_btn) self.table_widget.formlayout.addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample) submit_btn.clicked.connect(self.submit_new_sample)
logger.debug(f"Imported reagents: {self.reagents}")
def submit_new_sample(self): def submit_new_sample(self):
""" """
@@ -278,7 +274,9 @@ class App(QMainWindow):
if wanted_reagent == None: if wanted_reagent == None:
dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent]) dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent])
if dlg.exec(): if dlg.exec():
wanted_reagent = self.add_reagent(reagent_lot=reagents[reagent], reagent_type=reagent.replace("lot_", "")) logger.debug(f"checking reagent: {reagent} in self.reagents. Result: {self.reagents[reagent]}")
expiry_date = self.reagents[reagent]['exp']
wanted_reagent = self.add_reagent(reagent_lot=reagents[reagent], reagent_type=reagent.replace("lot_", ""), expiry=expiry_date)
else: else:
logger.debug("Will not add reagent.") logger.debug("Will not add reagent.")
if wanted_reagent != None: if wanted_reagent != None:
@@ -301,15 +299,17 @@ class App(QMainWindow):
for reagent in parsed_reagents: for reagent in parsed_reagents:
base_submission.reagents.append(reagent) base_submission.reagents.append(reagent)
# base_submission.reagents_id = reagent.id # base_submission.reagents_id = reagent.id
logger.debug("Checking kit integrity...")
kit_integrity = check_kit_integrity(base_submission)
if kit_integrity != None:
msg = AlertPop(message=kit_integrity['message'], status="critical")
msg.exec()
return
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.") logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
result = store_submission(ctx=self.ctx, base_submission=base_submission) result = store_submission(ctx=self.ctx, base_submission=base_submission)
# check result of storing for issues # # check result of storing for issues
if result != None: if result != None:
msg = QMessageBox() msg = AlertPop(result['message'])
# msg.setIcon(QMessageBox.critical)
msg.setText("Error")
msg.setInformativeText(result['message'])
msg.setWindowTitle("Error")
msg.exec() msg.exec()
# update summary sheet # update summary sheet
self.table_widget.sub_wid.setData() self.table_widget.sub_wid.setData()
@@ -318,7 +318,7 @@ class App(QMainWindow):
item.setParent(None) item.setParent(None)
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None): def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None):
""" """
Action to create new reagent in DB. Action to create new reagent in DB.
@@ -332,7 +332,7 @@ class App(QMainWindow):
if isinstance(reagent_lot, bool): if isinstance(reagent_lot, bool):
reagent_lot = "" reagent_lot = ""
# create form # create form
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type) dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry)
if dlg.exec(): if dlg.exec():
# extract form info # extract form info
labels, values = self.extract_form_info(dlg) labels, values = self.extract_form_info(dlg)
@@ -341,7 +341,7 @@ class App(QMainWindow):
# create reagent object # create reagent object
reagent = construct_reagent(ctx=self.ctx, info_dict=info) reagent = construct_reagent(ctx=self.ctx, info_dict=info)
# send reagent to db # send reagent to db
store_reagent(ctx=self.ctx, reagent=reagent) # store_reagent(ctx=self.ctx, reagent=reagent)
return reagent return reagent
@@ -417,19 +417,16 @@ class App(QMainWindow):
# set_column(idx, idx, max_len) # set column width # set_column(idx, idx, max_len) # set column width
# colu = worksheet.column_dimensions["C"] # colu = worksheet.column_dimensions["C"]
# style = NamedStyle(name="custom_currency", number_format='Currency') # style = NamedStyle(name="custom_currency", number_format='Currency')
for cell in worksheet['C']: for cell in worksheet['D']:
# try: # try:
# check = int(cell.row) # check = int(cell.row)
# except TypeError: # except TypeError:
# continue # continue
if cell.row > 3: if cell.row > 1:
cell.style = 'Currency' cell.style = 'Currency'
writer.close() writer.close()
def add_kit(self): def add_kit(self):
""" """
Constructs new kit from yaml and adds to DB. Constructs new kit from yaml and adds to DB.
@@ -477,7 +474,7 @@ class App(QMainWindow):
# correct start date being more recent than end date and rerun # correct start date being more recent than end date and rerun
if self.table_widget.datepicker.start_date.date() > self.table_widget.datepicker.end_date.date(): if self.table_widget.datepicker.start_date.date() > self.table_widget.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!") logger.warning("Start date after end date is not allowed!")
threemonthsago = self.table_widget.datepicker.end_date.date().addDays(-90) threemonthsago = self.table_widget.datepicker.end_date.date().addDays(-60)
# block signal that will rerun controls getter and set start date # block signal that will rerun controls getter and set start date
with QSignalBlocker(self.table_widget.datepicker.start_date) as blocker: with QSignalBlocker(self.table_widget.datepicker.start_date) as blocker:
self.table_widget.datepicker.start_date.setDate(threemonthsago) self.table_widget.datepicker.start_date.setDate(threemonthsago)
@@ -572,12 +569,14 @@ class App(QMainWindow):
if " " in sample: if " " in sample:
logger.warning(f"There is not supposed to be a space in the sample name!!!") logger.warning(f"There is not supposed to be a space in the sample name!!!")
sample = sample.replace(" ", "") sample = sample.replace(" ", "")
if sample not in ac_list: # if sample not in ac_list:
if not any([ac.startswith(sample) for ac in ac_list]):
continue continue
else: else:
for control in all_controls: for control in all_controls:
diff = difflib.SequenceMatcher(a=sample, b=control.name).ratio() diff = difflib.SequenceMatcher(a=sample, b=control.name).ratio()
if diff > 0.955: # if diff > 0.955:
if control.name.startswith(sample):
logger.debug(f"Checking {sample} against {control.name}... {diff}") logger.debug(f"Checking {sample} against {control.name}... {diff}")
# if sample == control.name: # if sample == control.name:
logger.debug(f"Found match:\n\tSample: {sample}\n\tControl: {control.name}\n\tDifference: {diff}") logger.debug(f"Found match:\n\tSample: {sample}\n\tControl: {control.name}\n\tDifference: {diff}")

View File

@@ -1,18 +1,17 @@
from datetime import date
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog, QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QTableView, QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
QTextEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox, QGridLayout, QPushButton, QSpinBox,
QScrollBar, QScrollArea, QHBoxLayout, QScrollBar, QHBoxLayout,
QMessageBox, QFileDialog, QToolBar QMessageBox
) )
from PyQt6.QtCore import Qt, QDate, QAbstractTableModel, QSize from PyQt6.QtCore import Qt, QDate, QSize
from PyQt6.QtGui import QFontMetrics, QAction # from PyQt6.QtGui import QFontMetrics, QAction
from backend.db import get_all_reagenttype_names, submissions_to_df, lookup_submission_by_id, lookup_all_sample_types, create_kit_from_yaml from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
@@ -26,55 +25,12 @@ else:
loader = FileSystemLoader(loader_path) loader = FileSystemLoader(loader_path)
env = Environment(loader=loader) env = Environment(loader=loader)
class AddReagentQuestion(QDialog):
"""
dialog to ask about adding a new reagne to db
"""
def __init__(self, reagent_type:str, reagent_lot:str) -> None:
super().__init__()
self.setWindowTitle(f"Add {reagent_lot}?")
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
message = QLabel(f"Couldn't find reagent type {reagent_type.replace('_', ' ').title().strip('Lot')}: {reagent_lot} in the database.\nWould you like to add it?")
self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class OverwriteSubQuestion(QDialog):
"""
dialog to ask about overwriting existing submission
"""
def __init__(self, message:str, rsl_plate_num:str) -> None:
super().__init__()
self.setWindowTitle(f"Overwrite {rsl_plate_num}?")
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
message = QLabel(message)
self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class AddReagentForm(QDialog): class AddReagentForm(QDialog):
""" """
dialog to add gather info about new reagent dialog to add gather info about new reagent
""" """
def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None) -> None: def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None, expiry:date|None=None) -> None:
super().__init__() super().__init__()
if reagent_lot == None: if reagent_lot == None:
@@ -92,7 +48,10 @@ class AddReagentForm(QDialog):
lot_input.setText(reagent_lot) lot_input.setText(reagent_lot)
# get expiry info # get expiry info
exp_input = QDateEdit(calendarPopup=True) exp_input = QDateEdit(calendarPopup=True)
exp_input.setDate(QDate.currentDate()) if expiry == None:
exp_input.setDate(QDate.currentDate())
else:
exp_input.setDate(expiry)
# get reagent type info # get reagent type info
type_input = QComboBox() type_input = QComboBox()
type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)]) type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)])
@@ -117,172 +76,6 @@ class AddReagentForm(QDialog):
self.setLayout(self.layout) self.setLayout(self.layout)
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
"""
def __init__(self, data) -> None:
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of rows in data
"""
return self._data.shape[0]
def columnCount(self, parnet=None) -> int:
"""
does what it says
Args:
parnet (_type_, optional): _description_. Defaults to None.
Returns:
int: number of columns in data
"""
return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str|None:
if index.isValid():
if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._data.columns[col]
return None
class SubmissionsSheet(QTableView):
"""
presents submission summary to user in tab1
"""
def __init__(self, ctx:dict) -> None:
"""
initialize
Args:
ctx (dict): settings passed from gui
"""
super().__init__()
self.ctx = ctx
self.setData()
self.resizeColumnsToContents()
self.resizeRowsToContents()
# self.clicked.connect(self.test)
self.doubleClicked.connect(self.show_details)
def setData(self) -> None:
"""
sets data in model
"""
self.data = submissions_to_df(ctx=self.ctx)
self.model = pandasModel(self.data)
self.setModel(self.model)
# self.resize(800,600)
def show_details(self) -> None:
"""
creates detailed data to show in seperate window
"""
index=(self.selectionModel().currentIndex())
# logger.debug(index)
value=index.sibling(index.row(),0).data()
dlg = SubmissionDetails(ctx=self.ctx, id=value)
# dlg.show()
if dlg.exec():
pass
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, ctx:dict, id:int) -> None:
super().__init__()
self.ctx = ctx
self.setWindowTitle("Submission Details")
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# get submision from db
data = lookup_submission_by_id(ctx=ctx, id=id)
self.base_dict = data.to_dict()
logger.debug(f"Base dict: {self.base_dict}")
# don't want id
del self.base_dict['id']
# convert sub objects to dicts
self.base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents]
self.base_dict['samples'] = [item.to_sub_dict() for item in data.samples]
# retrieve jinja template
template = env.get_template("submission_details.txt")
# render using object dict
text = template.render(sub=self.base_dict)
# create text field
txt_editor = QTextEdit(self)
txt_editor.setReadOnly(True)
txt_editor.document().setPlainText(text)
# resize
font = txt_editor.document().defaultFont()
fontMetrics = QFontMetrics(font)
textSize = fontMetrics.size(0, txt_editor.toPlainText())
w = textSize.width() + 10
h = textSize.height() + 10
txt_editor.setMinimumSize(w, h)
txt_editor.setMaximumSize(w, h)
txt_editor.resize(w, h)
interior.resize(w,900)
txt_editor.setText(text)
interior.setWidget(txt_editor)
self.layout = QVBoxLayout()
self.setFixedSize(w, 900)
btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(w)
btn.clicked.connect(self.export)
# def _create_actions(self):
# self.exportAction = QAction("Export", self)
def export(self):
template = env.get_template("submission_details.html")
html = template.render(sub=self.base_dict)
# logger.debug(f"Submission details: {self.base_dict}")
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
# logger.debug(f"report output name: {fname}")
# df.to_excel(fname, engine='openpyxl')
if fname.__str__() == ".":
logger.debug("Saving pdf was cancelled.")
return
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(html, dest=f)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
msg = QMessageBox()
msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()
class ReportDatePicker(QDialog): class ReportDatePicker(QDialog):
""" """
custom dialog to ask for report start/stop dates custom dialog to ask for report start/stop dates
@@ -467,7 +260,7 @@ class ControlsDatePicker(QWidget):
self.start_date = QDateEdit(calendarPopup=True) self.start_date = QDateEdit(calendarPopup=True)
# start date is three month prior to end date by default # start date is three month prior to end date by default
threemonthsago = QDate.currentDate().addDays(-90) threemonthsago = QDate.currentDate().addDays(-60)
self.start_date.setDate(threemonthsago) self.start_date.setDate(threemonthsago)
self.end_date = QDateEdit(calendarPopup=True) self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate()) self.end_date.setDate(QDate.currentDate())

View File

@@ -0,0 +1,78 @@
# from datetime import date
from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog,
QDialogButtonBox, QMessageBox
)
# from PyQt6.QtCore import Qt, QDate, QSize
# from PyQt6.QtGui import QFontMetrics, QAction
# from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml
from jinja2 import Environment, FileSystemLoader
import sys
from pathlib import Path
import logging
logger = logging.getLogger(f"submissions.{__name__}")
if getattr(sys, 'frozen', False):
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else:
loader_path = Path(__file__).parents[2].joinpath('templates').absolute().__str__()
loader = FileSystemLoader(loader_path)
env = Environment(loader=loader)
class AddReagentQuestion(QDialog):
"""
dialog to ask about adding a new reagne to db
"""
def __init__(self, reagent_type:str, reagent_lot:str) -> QDialog:
super().__init__()
self.setWindowTitle(f"Add {reagent_lot}?")
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
message = QLabel(f"Couldn't find reagent type {reagent_type.replace('_', ' ').title().strip('Lot')}: {reagent_lot} in the database.\n\nWould you like to add it?")
self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class OverwriteSubQuestion(QDialog):
"""
dialog to ask about overwriting existing submission
"""
def __init__(self, message:str, rsl_plate_num:str) -> QDialog:
super().__init__()
self.setWindowTitle(f"Overwrite {rsl_plate_num}?")
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
message = QLabel(message)
self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class AlertPop(QMessageBox):
def __init__(self, message:str, status:str) -> QMessageBox:
super().__init__()
icon = getattr(QMessageBox.Icon, status.title())
self.setIcon(icon)
# msg.setText("Error")
self.setInformativeText(message)
self.setWindowTitle(status.title())

View File

@@ -0,0 +1,188 @@
from datetime import date
from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea,
QMessageBox, QFileDialog
)
from PyQt6.QtCore import Qt, QAbstractTableModel
from PyQt6.QtGui import QFontMetrics
from backend.db import submissions_to_df, lookup_submission_by_id, lookup_all_sample_types, create_kit_from_yaml
from jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa
import sys
from pathlib import Path
import logging
logger = logging.getLogger(f"submissions.{__name__}")
if getattr(sys, 'frozen', False):
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else:
loader_path = Path(__file__).parents[2].joinpath('templates').absolute().__str__()
loader = FileSystemLoader(loader_path)
env = Environment(loader=loader)
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
"""
def __init__(self, data) -> None:
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of rows in data
"""
return self._data.shape[0]
def columnCount(self, parnet=None) -> int:
"""
does what it says
Args:
parnet (_type_, optional): _description_. Defaults to None.
Returns:
int: number of columns in data
"""
return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str|None:
if index.isValid():
if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._data.columns[col]
return None
class SubmissionsSheet(QTableView):
"""
presents submission summary to user in tab1
"""
def __init__(self, ctx:dict) -> None:
"""
initialize
Args:
ctx (dict): settings passed from gui
"""
super().__init__()
self.ctx = ctx
self.setData()
self.resizeColumnsToContents()
self.resizeRowsToContents()
# self.clicked.connect(self.test)
self.doubleClicked.connect(self.show_details)
def setData(self) -> None:
"""
sets data in model
"""
self.data = submissions_to_df(ctx=self.ctx)
self.model = pandasModel(self.data)
self.setModel(self.model)
# self.resize(800,600)
def show_details(self) -> None:
"""
creates detailed data to show in seperate window
"""
index=(self.selectionModel().currentIndex())
# logger.debug(index)
value=index.sibling(index.row(),0).data()
dlg = SubmissionDetails(ctx=self.ctx, id=value)
# dlg.show()
if dlg.exec():
pass
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, ctx:dict, id:int) -> None:
super().__init__()
self.ctx = ctx
self.setWindowTitle("Submission Details")
# create scrollable interior
interior = QScrollArea()
interior.setParent(self)
# get submision from db
data = lookup_submission_by_id(ctx=ctx, id=id)
self.base_dict = data.to_dict()
logger.debug(f"Base dict: {self.base_dict}")
# don't want id
del self.base_dict['id']
# convert sub objects to dicts
self.base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents]
self.base_dict['samples'] = [item.to_sub_dict() for item in data.samples]
# retrieve jinja template
template = env.get_template("submission_details.txt")
# render using object dict
text = template.render(sub=self.base_dict)
# create text field
txt_editor = QTextEdit(self)
txt_editor.setReadOnly(True)
txt_editor.document().setPlainText(text)
# resize
font = txt_editor.document().defaultFont()
fontMetrics = QFontMetrics(font)
textSize = fontMetrics.size(0, txt_editor.toPlainText())
w = textSize.width() + 10
h = textSize.height() + 10
txt_editor.setMinimumSize(w, h)
txt_editor.setMaximumSize(w, h)
txt_editor.resize(w, h)
interior.resize(w,900)
txt_editor.setText(text)
interior.setWidget(txt_editor)
self.layout = QVBoxLayout()
self.setFixedSize(w, 900)
btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(w)
btn.clicked.connect(self.export)
# def _create_actions(self):
# self.exportAction = QAction("Export", self)
def export(self):
template = env.get_template("submission_details.html")
html = template.render(sub=self.base_dict)
# logger.debug(f"Submission details: {self.base_dict}")
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
# logger.debug(f"report output name: {fname}")
# df.to_excel(fname, engine='openpyxl')
if fname.__str__() == ".":
logger.debug("Saving pdf was cancelled.")
return
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(html, dest=f)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
msg = QMessageBox()
msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()

View File

@@ -0,0 +1,31 @@
# from ..models import *
from backend.db.models import *
import logging
import numpy as np
logger = logging.getLogger(f"submissions.{__name__}")
def check_kit_integrity(sub:BasicSubmission):
ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types]
logger.debug(f"Kit reagents: {ext_kit_rtypes}")
reagenttypes = [reagent.type.name for reagent in sub.reagents]
logger.debug(f"Submission reagents: {reagenttypes}")
check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}")
common = list(set(ext_kit_rtypes).intersection(reagenttypes))
logger.debug(f"common reagents types: {common}")
if check:
result = None
else:
result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[x.upper() for x in ext_kit_rtypes if x not in common]}\n\nAlternatively, you may have set the wrong extraction kit."}
return result
def check_not_nan(cell_contents) -> bool:
try:
return not np.isnan(cell_contents)
except ValueError:
return True
except Exception as e:
logger.debug(f"Check encounteded unknown error: {e}")
return False