Added importing of PCR results.
This commit is contained in:
@@ -1,6 +1,22 @@
|
||||
# __init__.py
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Version of the realpython-reader package
|
||||
__version__ = "202303.3b"
|
||||
__project__ = "submissions"
|
||||
__version__ = "202303.4b"
|
||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||
__copyright__ = "2022-2023, Government of Canada"
|
||||
|
||||
project_path = Path(__file__).parents[2].absolute()
|
||||
|
||||
class bcolors:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
@@ -5,6 +5,7 @@ Convenience functions for interacting with the database.
|
||||
from . import models
|
||||
from .models.kits import reagenttypes_kittypes
|
||||
from .models.submissions import reagents_submissions
|
||||
from .models.samples import WWSample
|
||||
import pandas as pd
|
||||
import sqlalchemy.exc
|
||||
import sqlite3
|
||||
@@ -392,7 +393,11 @@ def submissions_to_df(ctx:dict, sub_type:str|None=None) -> pd.DataFrame:
|
||||
try:
|
||||
df = df.drop("ext_info", axis=1)
|
||||
except:
|
||||
logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.")
|
||||
logger.warning(f"Couldn't drop 'ext_info' column from submissionsheet df.")
|
||||
try:
|
||||
df = df.drop("pcr_info", axis=1)
|
||||
except:
|
||||
logger.warning(f"Couldn't drop 'pcr_info' column from submissionsheet df.")
|
||||
return df
|
||||
|
||||
|
||||
@@ -676,4 +681,39 @@ def delete_submission_by_id(ctx:dict, id:int) -> None:
|
||||
for sample in sub.samples:
|
||||
ctx['database_session'].delete(sample)
|
||||
ctx["database_session"].delete(sub)
|
||||
ctx["database_session"].commit()
|
||||
|
||||
|
||||
def lookup_ww_sample_by_rsl_sample_number(ctx:dict, rsl_number:str) -> models.WWSample:
|
||||
"""
|
||||
Retrieves wastewater sampel from database by rsl sample number
|
||||
|
||||
Args:
|
||||
ctx (dict): settings passed dwon from gui
|
||||
rsl_number (str): sample number assigned by robotics lab
|
||||
|
||||
Returns:
|
||||
models.WWSample: instance of wastewater sample
|
||||
"""
|
||||
return ctx['database_session'].query(models.WWSample).filter(models.WWSample.rsl_number==rsl_number).first()
|
||||
|
||||
|
||||
def update_ww_sample(ctx:dict, sample_obj:dict):
|
||||
"""
|
||||
Retrieves wastewater sample by rsl number (sample_obj['sample']) and updates values from constructed dictionary
|
||||
|
||||
Args:
|
||||
ctx (dict): settings passed down from gui
|
||||
sample_obj (dict): dictionary representing new values for database object
|
||||
"""
|
||||
ww_samp = lookup_ww_sample_by_rsl_sample_number(ctx=ctx, rsl_number=sample_obj['sample'])
|
||||
if ww_samp != None:
|
||||
for key, value in sample_obj.items():
|
||||
logger.debug(f"Setting {key} to {value}")
|
||||
# set attribute 'key' to 'value'
|
||||
setattr(ww_samp, key, value)
|
||||
else:
|
||||
logger.error(f"Unable to find sample {sample_obj['sample']}")
|
||||
return
|
||||
ctx['database_session'].add(ww_samp)
|
||||
ctx["database_session"].commit()
|
||||
@@ -2,7 +2,7 @@
|
||||
All models for individual samples.
|
||||
'''
|
||||
from . import Base
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN
|
||||
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@ class WWSample(Base):
|
||||
testing_type = Column(String(64))
|
||||
site_status = Column(String(64))
|
||||
notes = Column(String(2000))
|
||||
ct_n1 = Column(FLOAT(2))
|
||||
ct_n2 = Column(FLOAT(2))
|
||||
ct_n1 = Column(FLOAT(2)) #: AKA ct for N1
|
||||
ct_n2 = Column(FLOAT(2)) #: AKA ct for N2
|
||||
seq_submitted = Column(BOOLEAN())
|
||||
ww_seq_run_id = Column(String(64))
|
||||
sample_type = Column(String(8))
|
||||
pcr_results = Column(JSON)
|
||||
|
||||
|
||||
def to_string(self) -> str:
|
||||
@@ -48,9 +49,13 @@ class WWSample(Base):
|
||||
Returns:
|
||||
dict: well location and id NOTE: keys must sync with BCSample to_sub_dict below
|
||||
"""
|
||||
if self.ct_n1 != None and self.ct_n2 != None:
|
||||
name = f"{self.ww_sample_full_id}\n\t- ct N1: {'{:.2f}'.format(self.ct_n1)}, ct N2: {'{:.2f}'.format(self.ct_n1)}"
|
||||
else:
|
||||
name = self.ww_sample_full_id
|
||||
return {
|
||||
"well": self.well_number,
|
||||
"name": self.ww_sample_full_id,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -179,5 +179,20 @@ class Wastewater(BasicSubmission):
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
samples = relationship("WWSample", back_populates="rsl_plate", uselist=True)
|
||||
pcr_info = Column(JSON)
|
||||
# ww_sample_id = Column(String, ForeignKey("_ww_samples.id", ondelete="SET NULL", name="fk_WW_sample_id"))
|
||||
__mapper_args__ = {"polymorphic_identity": "wastewater", "polymorphic_load": "inline"}
|
||||
__mapper_args__ = {"polymorphic_identity": "wastewater", "polymorphic_load": "inline"}
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Extends parent class method to add controls to dict
|
||||
|
||||
Returns:
|
||||
dict: dictionary used in submissions summary
|
||||
"""
|
||||
output = super().to_dict()
|
||||
try:
|
||||
output['pcr_info'] = json.loads(self.pcr_info)
|
||||
except TypeError as e:
|
||||
pass
|
||||
return output
|
||||
@@ -1,16 +1,18 @@
|
||||
'''
|
||||
contains parser object for pulling values from client generated submission sheets.
|
||||
'''
|
||||
from getpass import getuser
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from backend.db.models import WWSample, BCSample
|
||||
from backend.db import lookup_ww_sample_by_rsl_sample_number
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
import numpy as np
|
||||
from datetime import date
|
||||
import uuid
|
||||
from tools import check_not_nan
|
||||
from tools import check_not_nan, retrieve_rsl_number
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -203,6 +205,7 @@ class SheetParser(object):
|
||||
sample_parser = SampleParser(submission_info.iloc[16:40])
|
||||
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
|
||||
self.sub['samples'] = sample_parse()
|
||||
self.sub['csv'] = self.xl.parse("Copy to import file", dtype=object)
|
||||
|
||||
|
||||
class SampleParser(object):
|
||||
@@ -284,3 +287,112 @@ class SampleParser(object):
|
||||
new.well_number = sample['Unnamed: 1']
|
||||
new_list.append(new)
|
||||
return new_list
|
||||
|
||||
|
||||
class PCRParser(object):
|
||||
"""
|
||||
Object to pull data from Design and Analysis PCR export file.
|
||||
"""
|
||||
def __init__(self, ctx:dict, filepath:Path|None = None) -> None:
|
||||
"""
|
||||
Initializes object.
|
||||
|
||||
Args:
|
||||
ctx (dict): settings passed down from gui.
|
||||
filepath (Path | None, optional): file to parse. Defaults to None.
|
||||
"""
|
||||
self.ctx = ctx
|
||||
logger.debug(f"Parsing {filepath.__str__()}")
|
||||
if filepath == None:
|
||||
logger.error(f"No filepath given.")
|
||||
self.xl = None
|
||||
else:
|
||||
try:
|
||||
self.xl = pd.ExcelFile(filepath.__str__())
|
||||
except ValueError as e:
|
||||
logger.error(f"Incorrect value: {e}")
|
||||
self.xl = None
|
||||
except PermissionError:
|
||||
logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.")
|
||||
return
|
||||
# self.pcr = OrderedDict()
|
||||
self.pcr = {}
|
||||
self.plate_num, self.submission_type = retrieve_rsl_number(filepath.__str__())
|
||||
logger.debug(f"Set plate number to {self.plate_num} and type to {self.submission_type}")
|
||||
self.samples = []
|
||||
parser = getattr(self, f"parse_{self.submission_type}")
|
||||
parser()
|
||||
|
||||
|
||||
def parse_general(self, sheet_name:str):
|
||||
"""
|
||||
Parse general info rows for all types of PCR results
|
||||
|
||||
Args:
|
||||
sheet_name (str): Name of sheet in excel workbook that holds info.
|
||||
"""
|
||||
df = self.xl.parse(sheet_name=sheet_name, dtype=object).fillna("")
|
||||
# self.pcr['file'] = df.iloc[1][1]
|
||||
self.pcr['comment'] = df.iloc[0][1]
|
||||
self.pcr['operator'] = df.iloc[1][1]
|
||||
self.pcr['barcode'] = df.iloc[2][1]
|
||||
self.pcr['instrument'] = df.iloc[3][1]
|
||||
self.pcr['block_type'] = df.iloc[4][1]
|
||||
self.pcr['instrument_name'] = df.iloc[5][1]
|
||||
self.pcr['instrument_serial'] = df.iloc[6][1]
|
||||
self.pcr['heated_cover_serial'] = df.iloc[7][1]
|
||||
self.pcr['block_serial'] = df.iloc[8][1]
|
||||
self.pcr['run-start'] = df.iloc[9][1]
|
||||
self.pcr['run_end'] = df.iloc[10][1]
|
||||
self.pcr['run_duration'] = df.iloc[11][1]
|
||||
self.pcr['sample_volume'] = df.iloc[12][1]
|
||||
self.pcr['cover_temp'] = df.iloc[13][1]
|
||||
self.pcr['passive_ref'] = df.iloc[14][1]
|
||||
self.pcr['pcr_step'] = df.iloc[15][1]
|
||||
self.pcr['quant_cycle_method'] = df.iloc[16][1]
|
||||
self.pcr['analysis_time'] = df.iloc[17][1]
|
||||
self.pcr['software'] = df.iloc[18][1]
|
||||
self.pcr['plugin'] = df.iloc[19][1]
|
||||
self.pcr['exported_on'] = df.iloc[20][1]
|
||||
self.pcr['imported_by'] = getuser()
|
||||
return df
|
||||
|
||||
def parse_wastewater(self):
|
||||
"""
|
||||
Parse specific to wastewater samples.
|
||||
"""
|
||||
df = self.parse_general(sheet_name="Results")
|
||||
self.samples_df = df.iloc[23:][0:]
|
||||
# iloc is [row][column]
|
||||
for ii, row in self.samples_df.iterrows():
|
||||
try:
|
||||
sample_obj = [sample for sample in self.samples if sample['sample'] == row[3]][0]
|
||||
except IndexError:
|
||||
sample_obj = dict(
|
||||
sample = row[3],
|
||||
)
|
||||
logger.debug(f"Got sample obj: {sample_obj}")
|
||||
# logger.debug(f"row: {row}")
|
||||
# rsl_num = row[3]
|
||||
# # logger.debug(f"Looking up: {rsl_num}")
|
||||
# ww_samp = lookup_ww_sample_by_rsl_sample_number(ctx=self.ctx, rsl_number=rsl_num)
|
||||
# logger.debug(f"Got: {ww_samp}")
|
||||
match row[4]:
|
||||
case "N1":
|
||||
if isinstance(row[12], float):
|
||||
sample_obj['ct_n1'] = row[12]
|
||||
else:
|
||||
sample_obj['ct_n1'] = 0.0
|
||||
case "N2":
|
||||
if isinstance(row[12], float):
|
||||
sample_obj['ct_n2'] = row[12]
|
||||
else:
|
||||
sample_obj['ct_n2'] = 0.0
|
||||
case _:
|
||||
logger.warning(f"Unexpected input for row[4]: {row[4]}")
|
||||
self.samples.append(sample_obj)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'''
|
||||
Operations for all user interactions.
|
||||
'''
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
@@ -23,15 +24,15 @@ from xhtml2pdf import pisa
|
||||
# import plotly.express as px
|
||||
import yaml
|
||||
import pprint
|
||||
from backend.excel import convert_data_list_to_df, make_report_xlsx, make_report_html, SheetParser
|
||||
from backend.excel import convert_data_list_to_df, make_report_xlsx, make_report_html, SheetParser, PCRParser
|
||||
from backend.db import (construct_submission_info, lookup_reagent,
|
||||
construct_reagent, store_submission, lookup_kittype_by_use,
|
||||
lookup_regent_by_type_name, lookup_all_orgs, lookup_submissions_by_date_range,
|
||||
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,
|
||||
create_org_from_yaml, store_reagent
|
||||
create_org_from_yaml, store_reagent, lookup_ww_sample_by_rsl_sample_number, lookup_kittype_by_name,
|
||||
update_ww_sample
|
||||
)
|
||||
from backend.db import lookup_kittype_by_name
|
||||
from .functions import extract_form_info
|
||||
from tools import check_not_nan, check_kit_integrity, check_if_app
|
||||
# from backend.excel.reports import
|
||||
@@ -89,9 +90,11 @@ class App(QMainWindow):
|
||||
helpMenu.addAction(self.helpAction)
|
||||
helpMenu.addAction(self.docsAction)
|
||||
fileMenu.addAction(self.importAction)
|
||||
fileMenu.addAction(self.importPCRAction)
|
||||
reportMenu.addAction(self.generateReportAction)
|
||||
maintenanceMenu.addAction(self.joinControlsAction)
|
||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||
maintenanceMenu.addAction(self.joinPCRAction)
|
||||
|
||||
def _createToolBar(self):
|
||||
"""
|
||||
@@ -107,13 +110,15 @@ class App(QMainWindow):
|
||||
"""
|
||||
creates actions
|
||||
"""
|
||||
self.importAction = QAction("&Import", self)
|
||||
self.importAction = QAction("&Import Submission", self)
|
||||
self.importPCRAction = QAction("&Import PCR Results", self)
|
||||
self.addReagentAction = QAction("Add Reagent", self)
|
||||
self.generateReportAction = QAction("Make Report", self)
|
||||
self.addKitAction = QAction("Import Kit", self)
|
||||
self.addOrgAction = QAction("Import Org", self)
|
||||
self.joinControlsAction = QAction("Link Controls")
|
||||
self.joinExtractionAction = QAction("Link Ext Logs")
|
||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||
self.joinPCRAction = QAction("Link PCR Logs")
|
||||
self.helpAction = QAction("&About", self)
|
||||
self.docsAction = QAction("&Docs", self)
|
||||
|
||||
@@ -123,6 +128,7 @@ class App(QMainWindow):
|
||||
connect menu and tool bar item to functions
|
||||
"""
|
||||
self.importAction.triggered.connect(self.importSubmission)
|
||||
self.importPCRAction.triggered.connect(self.importPCRResults)
|
||||
self.addReagentAction.triggered.connect(self.add_reagent)
|
||||
self.generateReportAction.triggered.connect(self.generateReport)
|
||||
self.addKitAction.triggered.connect(self.add_kit)
|
||||
@@ -133,6 +139,7 @@ class App(QMainWindow):
|
||||
self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
|
||||
self.joinControlsAction.triggered.connect(self.linkControls)
|
||||
self.joinExtractionAction.triggered.connect(self.linkExtractions)
|
||||
self.joinPCRAction.triggered.connect(self.linkPCR)
|
||||
self.helpAction.triggered.connect(self.showAbout)
|
||||
self.docsAction.triggered.connect(self.openDocs)
|
||||
|
||||
@@ -180,7 +187,8 @@ class App(QMainWindow):
|
||||
(?P<submitted_date>^submitted_date$) |
|
||||
(?P<submitting_lab>)^submitting_lab$ |
|
||||
(?P<samples>)^samples$ |
|
||||
(?P<reagent>^lot_.*$)
|
||||
(?P<reagent>^lot_.*$) |
|
||||
(?P<csv>^csv$)
|
||||
""", re.VERBOSE)
|
||||
for item in prsr.sub:
|
||||
logger.debug(f"Item: {item}")
|
||||
@@ -213,14 +221,19 @@ class App(QMainWindow):
|
||||
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
|
||||
msg.exec()
|
||||
# create combobox to hold looked up kits
|
||||
# add_widget = KitSelector(ctx=self.ctx, submission_type=prsr.sub['submission_type'], parent=self)
|
||||
add_widget = QComboBox()
|
||||
# add_widget.currentTextChanged.connect(self.kit_reload)
|
||||
# lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
uses = [item.__str__() for item in lookup_kittype_by_use(ctx=self.ctx, used_by=prsr.sub['submission_type'])]
|
||||
# if len(uses) > 0:
|
||||
add_widget.addItems(uses)
|
||||
# else:
|
||||
# add_widget.addItems(['bacterial_culture'])
|
||||
self.ext_kit = prsr.sub[item]
|
||||
if check_not_nan(prsr.sub[item]):
|
||||
self.ext_kit = prsr.sub[item]
|
||||
else:
|
||||
self.ext_kit = add_widget.currentText()
|
||||
case 'submitted_date':
|
||||
# create label
|
||||
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
|
||||
@@ -234,7 +247,9 @@ class App(QMainWindow):
|
||||
add_widget.setDate(date.today())
|
||||
case 'reagent':
|
||||
# create label
|
||||
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
|
||||
reg_label = QLabel(item.replace("_", " ").title())
|
||||
reg_label.setObjectName(f"lot_{item}_label")
|
||||
self.table_widget.formlayout.addWidget(reg_label)
|
||||
# create reagent choice widget
|
||||
add_widget = ImportReagent(ctx=self.ctx, item=item, prsr=prsr)
|
||||
self.reagents[item] = prsr.sub[item]
|
||||
@@ -243,10 +258,13 @@ class App(QMainWindow):
|
||||
logger.debug(f"{item}: {prsr.sub[item]}")
|
||||
self.samples = prsr.sub[item]
|
||||
add_widget = None
|
||||
case 'csv':
|
||||
self.csv = prsr.sub[item]
|
||||
case _:
|
||||
# anything else gets added in as a line edit
|
||||
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
|
||||
add_widget = QLineEdit()
|
||||
logger.debug(f"Setting widget text to {str(prsr.sub[item]).replace('_', ' ')}")
|
||||
add_widget.setText(str(prsr.sub[item]).replace("_", " "))
|
||||
try:
|
||||
add_widget.setObjectName(item)
|
||||
@@ -256,21 +274,59 @@ class App(QMainWindow):
|
||||
logger.error(e)
|
||||
# compare self.reagents with expected reagents in kit
|
||||
if hasattr(self, 'ext_kit'):
|
||||
kit = lookup_kittype_by_name(ctx=self.ctx, name=self.ext_kit)
|
||||
kit_integrity = check_kit_integrity(kit, [item.replace("lot_", "") for item in self.reagents])
|
||||
if kit_integrity != None:
|
||||
msg = AlertPop(message=kit_integrity['message'], status="critical")
|
||||
msg.exec()
|
||||
for item in kit_integrity['missing']:
|
||||
self.table_widget.formlayout.addWidget(QLabel(f"Lot {item.replace('_', ' ').title()}"))
|
||||
add_widget = ImportReagent(ctx=self.ctx, item=item)
|
||||
self.table_widget.formlayout.addWidget(add_widget)
|
||||
self.kit_integrity_completion()
|
||||
# create submission button
|
||||
# submit_btn = QPushButton("Submit")
|
||||
# submit_btn.setObjectName("submit_btn")
|
||||
# self.table_widget.formlayout.addWidget(submit_btn)
|
||||
# submit_btn.clicked.connect(self.submit_new_sample)
|
||||
logger.debug(f"Imported reagents: {self.reagents}")
|
||||
|
||||
|
||||
def kit_reload(self):
|
||||
"""
|
||||
Removes all reagents from form before running kit integrity completion.
|
||||
"""
|
||||
for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
|
||||
# item.setParent(None)
|
||||
if isinstance(item, QLabel):
|
||||
if item.text().startswith("Lot"):
|
||||
item.setParent(None)
|
||||
else:
|
||||
logger.debug(f"Type of {item.objectName()} is {type(item)}")
|
||||
if item.objectName().startswith("lot_"):
|
||||
item.setParent(None)
|
||||
self.kit_integrity_completion()
|
||||
|
||||
|
||||
def kit_integrity_completion(self):
|
||||
"""
|
||||
Performs check of imported reagents
|
||||
NOTE: this will not change self.reagents which should be fine
|
||||
since it's only used when looking up
|
||||
"""
|
||||
logger.debug(inspect.currentframe().f_back.f_code.co_name)
|
||||
kit_widget = self.table_widget.formlayout.parentWidget().findChild(QComboBox, 'extraction_kit')
|
||||
logger.debug(f"Kit selector: {kit_widget}")
|
||||
self.ext_kit = kit_widget.currentText()
|
||||
logger.debug(f"Checking integrity of {self.ext_kit}")
|
||||
kit = lookup_kittype_by_name(ctx=self.ctx, name=self.ext_kit)
|
||||
reagents_to_lookup = [item.replace("lot_", "") for item in self.reagents]
|
||||
logger.debug(f"Reagents for lookup for {kit.name}: {reagents_to_lookup}")
|
||||
kit_integrity = check_kit_integrity(kit, reagents_to_lookup)
|
||||
if kit_integrity != None:
|
||||
msg = AlertPop(message=kit_integrity['message'], status="critical")
|
||||
msg.exec()
|
||||
for item in kit_integrity['missing']:
|
||||
self.table_widget.formlayout.addWidget(QLabel(f"Lot {item.replace('_', ' ').title()}"))
|
||||
add_widget = ImportReagent(ctx=self.ctx, item=item)
|
||||
self.table_widget.formlayout.addWidget(add_widget)
|
||||
submit_btn = QPushButton("Submit")
|
||||
submit_btn.setObjectName("lot_submit_btn")
|
||||
self.table_widget.formlayout.addWidget(submit_btn)
|
||||
submit_btn.clicked.connect(self.submit_new_sample)
|
||||
logger.debug(f"Imported reagents: {self.reagents}")
|
||||
|
||||
|
||||
def submit_new_sample(self):
|
||||
"""
|
||||
Attempt to add sample to database when 'submit' button clicked
|
||||
@@ -341,6 +397,15 @@ class App(QMainWindow):
|
||||
# reset form
|
||||
for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
|
||||
item.setParent(None)
|
||||
if hasattr(self, 'csv'):
|
||||
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
|
||||
if dlg.exec():
|
||||
home_dir = Path(self.ctx["directory_path"]).joinpath(f"{base_submission.rsl_plate_num}.csv").resolve().__str__()
|
||||
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".csv")[0])
|
||||
try:
|
||||
self.csv.to_csv(fname.__str__(), index=False)
|
||||
except PermissionError:
|
||||
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
|
||||
|
||||
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None):
|
||||
@@ -468,8 +533,6 @@ class App(QMainWindow):
|
||||
msg.exec()
|
||||
|
||||
|
||||
|
||||
|
||||
def _controls_getter(self):
|
||||
"""
|
||||
Lookup controls from database and send to chartmaker
|
||||
@@ -550,6 +613,9 @@ class App(QMainWindow):
|
||||
|
||||
|
||||
def linkControls(self):
|
||||
"""
|
||||
Adds controls pulled from irida to relevant submissions
|
||||
"""
|
||||
all_bcs = lookup_all_submissions_by_type(self.ctx, "Bacterial Culture")
|
||||
logger.debug(all_bcs)
|
||||
all_controls = get_all_controls(self.ctx)
|
||||
@@ -598,6 +664,9 @@ class App(QMainWindow):
|
||||
|
||||
|
||||
def linkExtractions(self):
|
||||
"""
|
||||
Links extraction logs from .csv files to relevant submissions.
|
||||
"""
|
||||
home_dir = str(Path(self.ctx["directory_path"]))
|
||||
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir, filter = "csv(*.csv)")[0])
|
||||
with open(fname.__str__(), 'r') as f:
|
||||
@@ -648,6 +717,107 @@ class App(QMainWindow):
|
||||
dlg.exec()
|
||||
|
||||
|
||||
def linkPCR(self):
|
||||
"""
|
||||
Links PCR logs from .csv files to relevant submissions.
|
||||
"""
|
||||
home_dir = str(Path(self.ctx["directory_path"]))
|
||||
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir, filter = "csv(*.csv)")[0])
|
||||
with open(fname.__str__(), 'r') as f:
|
||||
runs = [col.strip().split(",") for col in f.readlines()]
|
||||
count = 0
|
||||
for run in runs:
|
||||
obj = dict(
|
||||
start_time=run[0].strip(),
|
||||
rsl_plate_num=run[1].strip(),
|
||||
biomek_status=run[2].strip(),
|
||||
quant_status=run[3].strip(),
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# for ii in range(6, len(run)):
|
||||
# obj[f"column{str(ii-5)}_vol"] = run[ii]
|
||||
sub = lookup_submission_by_rsl_num(ctx=self.ctx, rsl_num=obj['rsl_plate_num'])
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
continue
|
||||
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
|
||||
existing = json.loads(sub.pcr_info)
|
||||
else:
|
||||
existing = None
|
||||
try:
|
||||
if json.dumps(obj) in sub.pcr_info:
|
||||
logger.debug(f"Looks like we already have that info.")
|
||||
continue
|
||||
else:
|
||||
count += 1
|
||||
except TypeError:
|
||||
logger.error(f"No json to dump")
|
||||
if existing != None:
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(obj)}: {obj}")
|
||||
existing.append(obj)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.pcr_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.pcr_info = json.dumps([obj])
|
||||
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.pcr_info}")
|
||||
else:
|
||||
sub.pcr_info = json.dumps([obj])
|
||||
self.ctx['database_session'].add(sub)
|
||||
self.ctx["database_session"].commit()
|
||||
dlg = AlertPop(message=f"We added {count} logs to the database.", status='information')
|
||||
dlg.exec()
|
||||
|
||||
|
||||
def importPCRResults(self):
|
||||
"""
|
||||
Imports results exported from Design and Analysis .eds files
|
||||
"""
|
||||
home_dir = str(Path(self.ctx["directory_path"]))
|
||||
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir, filter = "xlsx(*.xlsx)")[0])
|
||||
parser = PCRParser(ctx=self.ctx, filepath=fname)
|
||||
logger.debug(f"Attempting lookup for {parser.plate_num}")
|
||||
sub = lookup_submission_by_rsl_num(ctx=self.ctx, rsl_num=parser.plate_num)
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
logger.error(f"Submission of number {parser.plate_num} not found.")
|
||||
return
|
||||
# jout = json.dumps(parser.pcr)
|
||||
count = 0
|
||||
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
|
||||
existing = json.loads(sub.pcr_info)
|
||||
else:
|
||||
# jout = None
|
||||
existing = None
|
||||
if existing != None:
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(parser.pcr)}: {parser.pcr}")
|
||||
if json.dumps(parser.pcr) not in sub.pcr_info:
|
||||
existing.append(parser.pcr)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.pcr_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.pcr_info = json.dumps([parser.pcr])
|
||||
logger.debug(f"Final pcr info for {sub.rsl_plate_num}: {sub.pcr_info}")
|
||||
else:
|
||||
sub.pcr_info = json.dumps([parser.pcr])
|
||||
self.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)}")
|
||||
self.ctx["database_session"].commit()
|
||||
logger.debug(f"Got {len(parser.samples)} to update!")
|
||||
for sample in parser.samples:
|
||||
logger.debug(f"Running update on: {sample['sample']}")
|
||||
update_ww_sample(ctx=self.ctx, sample_obj=sample)
|
||||
dlg = AlertPop(message=f"We added PCR info to {sub.rsl_plate_num}.", status='information')
|
||||
dlg.exec()
|
||||
|
||||
|
||||
class AddSubForm(QWidget):
|
||||
|
||||
def __init__(self, parent):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Contains miscellaneous widgets for frontend functions
|
||||
'''
|
||||
from datetime import date
|
||||
import typing
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout,
|
||||
QLineEdit, QComboBox, QDialog,
|
||||
@@ -10,10 +11,11 @@ from PyQt6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QDate, QSize
|
||||
# from submissions.backend.db.functions import lookup_kittype_by_use
|
||||
# from submissions.backend.db import lookup_regent_by_type_name_and_kit_name
|
||||
from tools import check_not_nan
|
||||
from ..functions import extract_form_info
|
||||
from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml, lookup_regent_by_type_name#, lookup_regent_by_type_name_and_kit_name
|
||||
from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml, lookup_regent_by_type_name, lookup_kittype_by_use#, lookup_regent_by_type_name_and_kit_name
|
||||
from backend.excel.parser import SheetParser
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import sys
|
||||
@@ -329,9 +331,13 @@ class ImportReagent(QComboBox):
|
||||
else:
|
||||
if len(relevant_reagents) > 1:
|
||||
logger.debug(f"Found {prsr.sub[item]['lot']} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
||||
relevant_reagents.insert(0, relevant_reagents.pop(relevant_reagents.index(prsr.sub[item]['lot'])))
|
||||
idx = relevant_reagents.index(str(prsr.sub[item]['lot']))
|
||||
logger.debug(f"The index we got for {prsr.sub[item]['lot']} in {relevant_reagents} was {idx}")
|
||||
moved_reag = relevant_reagents.pop(idx)
|
||||
relevant_reagents.insert(0, moved_reag)
|
||||
else:
|
||||
logger.debug(f"Found {prsr.sub[item]['lot']} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
|
||||
logger.debug(f"New relevant reagents: {relevant_reagents}")
|
||||
self.setObjectName(f"lot_{item}")
|
||||
self.addItems(relevant_reagents)
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
<head>
|
||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||
</head>
|
||||
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info'] %}
|
||||
<body>
|
||||
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>
|
||||
<p>{% for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' and key != 'ext_info' %}
|
||||
<p>{% for key, value in sub.items() if key not in excluded %}
|
||||
{% if loop.index == 1 %}
|
||||
{% if key=='Cost' %}{{ key }}: {{ "${:,.2f}".format(value) }}{% else %}{{ key }}: {{ value }}{% endif %}<br>
|
||||
{% else %}
|
||||
@@ -62,5 +63,25 @@
|
||||
{% endfor %}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if sub['pcr_info'] %}
|
||||
{% for entry in sub['pcr_info'] %}
|
||||
{% if 'comment' not in entry.keys() %}
|
||||
<h3><u>qPCR Momentum Status:</u></h3>
|
||||
{% else %}
|
||||
<h3><u>qPCR Status:</u></h3>
|
||||
{% endif %}
|
||||
<p>{% for key, value in entry.items() if key != 'imported_by'%}
|
||||
{% if loop.index == 1%}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}<br>
|
||||
{% else %}
|
||||
{% if "column" in key %}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}uL<br>
|
||||
{% else %}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}<br>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,7 @@
|
||||
{# template for constructing submission details #}
|
||||
|
||||
{% for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' and key != 'ext_info' %}
|
||||
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info'] %}
|
||||
{# for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' and key != 'ext_info' #}
|
||||
{% for key, value in sub.items() if key not in excluded %}
|
||||
{% if key=='Cost' %} {{ key }}: {{ "${:,.2f}".format(value) }} {% else %} {{ key }}: {{ value }} {% endif %}
|
||||
{% endfor %}
|
||||
Reagents:
|
||||
@@ -9,7 +10,7 @@ Reagents:
|
||||
{% if sub['samples']%}
|
||||
Samples:
|
||||
{% for item in sub['samples'] %}
|
||||
{{ item['well'] }}: {{ item['name'] }}{% endfor %}{% endif %}
|
||||
{{ item['well'] }}: {{ item['name'] }}{% endfor %}{% endif %}
|
||||
{% if sub['controls'] %}
|
||||
Attached Controls:
|
||||
{% for item in sub['controls'] %}
|
||||
@@ -22,6 +23,10 @@ Attached Controls:
|
||||
{% if sub['ext_info'] %}{% for entry in sub['ext_info'] %}
|
||||
Extraction Status:
|
||||
{% for key, value in entry.items() %}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}{% endfor %}{% endif %}
|
||||
{% if sub['pcr_info'] %}{% for entry in sub['pcr_info'] %}
|
||||
{% if 'comment' not in entry.keys() %}qPCR Momentum Status:{% else %}
|
||||
qPCR Status{% endif %}
|
||||
{% for key, value in entry.items() if key != 'imported_by' %}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'''
|
||||
Contains miscellaenous functions used by both frontend and backend.
|
||||
'''
|
||||
import re
|
||||
import sys
|
||||
import numpy as np
|
||||
import logging
|
||||
import getpass
|
||||
from backend.db.models import BasicSubmission, KitType
|
||||
from typing import Tuple
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -115,4 +117,15 @@ def check_if_app(ctx:dict=None) -> bool:
|
||||
if getattr(sys, 'frozen', False):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def retrieve_rsl_number(in_str:str) -> Tuple[str, str]:
|
||||
in_str = in_str.split("\\")[-1]
|
||||
logger.debug(f"Attempting match of {in_str}")
|
||||
regex = re.compile(r"""
|
||||
(?P<wastewater>RSL-WW-20\d{6})|(?P<bacterial_culture>RSL-\d{2}-\d{4})
|
||||
""", re.VERBOSE)
|
||||
m = regex.search(in_str)
|
||||
return (m.group(), m.lastgroup)
|
||||
|
||||
Reference in New Issue
Block a user