Large scale frontend refactor
This commit is contained in:
@@ -9,7 +9,8 @@ logger = setup_logger(verbosity=3)
|
||||
# create settings object
|
||||
ctx = get_config(None)
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from frontend import App
|
||||
# from frontend import App
|
||||
from frontend.widgets.app import App
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(['', '--no-sandbox'])
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
'''
|
||||
Contains database, validators and excel operations.
|
||||
'''
|
||||
'''
|
||||
from .db import *
|
||||
from .excel import *
|
||||
from .validators import *
|
||||
@@ -199,7 +199,7 @@ def update_subsampassoc_with_pcr(submission:BasicSubmission, sample:BasicSample,
|
||||
dict|None: result object
|
||||
"""
|
||||
# assoc = lookup_submission_sample_association(ctx, submission=submission, sample=sample)
|
||||
assoc = SubmissionSampleAssociation.query(submission=submission, sample=sample)
|
||||
assoc = SubmissionSampleAssociation.query(submission=submission, sample=sample, limit=1)
|
||||
for k,v in input_dict.items():
|
||||
try:
|
||||
setattr(assoc, k, v)
|
||||
@@ -209,28 +209,3 @@ def update_subsampassoc_with_pcr(submission:BasicSubmission, sample:BasicSample,
|
||||
result = assoc.save()
|
||||
return result
|
||||
|
||||
|
||||
# def store_object(ctx:Settings, object) -> dict|None:
|
||||
# """
|
||||
# Store an object in the database
|
||||
|
||||
# Args:
|
||||
# ctx (Settings): Settings object passed down from gui
|
||||
# object (_type_): Object to be stored
|
||||
|
||||
# Returns:
|
||||
# dict|None: Result of action
|
||||
# """
|
||||
# dbs = ctx.database_session
|
||||
# dbs.merge(object)
|
||||
# try:
|
||||
# dbs.commit()
|
||||
# except (SQLIntegrityError, AlcIntegrityError) as e:
|
||||
# logger.debug(f"Hit an integrity error : {e}")
|
||||
# dbs.rollback()
|
||||
# return {"message":f"This object {object} already exists, so we can't add it.\n{e}", "status":"Critical"}
|
||||
# except (SQLOperationalError, AlcOperationalError):
|
||||
# logger.error(f"Hit an operational error: {e}")
|
||||
# dbs.rollback()
|
||||
# return {"message":"The database is locked for editing."}
|
||||
# return None
|
||||
|
||||
@@ -310,9 +310,10 @@ class BasicSubmission(Base):
|
||||
return input_excel
|
||||
|
||||
@classmethod
|
||||
def enforce_name(cls, instr:str) -> str:
|
||||
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
|
||||
logger.debug(f"Hello from {cls.__mapper_args__['polymorphic_identity']} Enforcer!")
|
||||
logger.debug(f"Attempting enforcement on {instr}")
|
||||
logger.debug(f"Attempting enforcement on {instr} using data: {pformat(data)}")
|
||||
# sys.exit()
|
||||
return instr
|
||||
|
||||
@classmethod
|
||||
@@ -499,7 +500,7 @@ class BasicSubmission(Base):
|
||||
cls: _description_
|
||||
"""
|
||||
code = 0
|
||||
msg = None
|
||||
msg = ""
|
||||
disallowed = ["id"]
|
||||
if kwargs == {}:
|
||||
raise ValueError("Need to narrow down query or the first available instance will be returned.")
|
||||
@@ -636,9 +637,9 @@ class BacterialCulture(BasicSubmission):
|
||||
return input_excel
|
||||
|
||||
@classmethod
|
||||
def enforce_name(cls, instr:str) -> str:
|
||||
outstr = super().enforce_name(instr=instr)
|
||||
def construct() -> str:
|
||||
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
|
||||
outstr = super().enforce_name(instr=instr, data=data)
|
||||
def construct(data:dict|None=None) -> str:
|
||||
"""
|
||||
DEPRECIATED due to slowness. Search for the largest rsl number and increment by 1
|
||||
|
||||
@@ -765,18 +766,28 @@ class Wastewater(BasicSubmission):
|
||||
return samples
|
||||
|
||||
@classmethod
|
||||
def enforce_name(cls, instr:str) -> str:
|
||||
outstr = super().enforce_name(instr=instr)
|
||||
def construct():
|
||||
today = datetime.now()
|
||||
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
|
||||
outstr = super().enforce_name(instr=instr, data=data)
|
||||
def construct(data:dict|None=None):
|
||||
if "submitted_date" in data.keys():
|
||||
if data['submitted_date']['value'] != None:
|
||||
today = data['submitted_date']['value']
|
||||
else:
|
||||
today = datetime.now()
|
||||
else:
|
||||
today = re.search(r"\d{4}(_|-)?\d{2}(_|-)?\d{2}", instr)
|
||||
try:
|
||||
today = parse(today.group())
|
||||
except AttributeError:
|
||||
today = datetime.now()
|
||||
return f"RSL-WW-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
|
||||
if outstr == None:
|
||||
outstr = construct()
|
||||
outstr = construct(data)
|
||||
try:
|
||||
outstr = re.sub(r"PCR(-|_)", "", outstr)
|
||||
except AttributeError as e:
|
||||
logger.error(f"Problem using regex: {e}")
|
||||
outstr = construct()
|
||||
outstr = construct(data)
|
||||
outstr = outstr.replace("RSLWW", "RSL-WW")
|
||||
outstr = re.sub(r"WW(\d{4})", r"WW-\1", outstr, flags=re.IGNORECASE)
|
||||
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr)
|
||||
@@ -848,9 +859,9 @@ class WastewaterArtic(BasicSubmission):
|
||||
return input_dict
|
||||
|
||||
@classmethod
|
||||
def enforce_name(cls, instr:str) -> str:
|
||||
outstr = super().enforce_name(instr=instr)
|
||||
def construct():
|
||||
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
|
||||
outstr = super().enforce_name(instr=instr, data=data)
|
||||
def construct(data:dict|None=None):
|
||||
today = datetime.now()
|
||||
return f"RSL-AR-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
contains parser object for pulling values from client generated submission sheets.
|
||||
'''
|
||||
from getpass import getuser
|
||||
import pprint
|
||||
from pprint import pformat
|
||||
from typing import List
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
@@ -15,7 +15,7 @@ import re
|
||||
from datetime import date
|
||||
from dateutil.parser import parse, ParserError
|
||||
from tools import check_not_nan, convert_nans_to_nones, Settings
|
||||
from frontend.custom_widgets.pop_ups import KitSelector
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -70,7 +70,7 @@ class SheetParser(object):
|
||||
pass
|
||||
case _:
|
||||
self.sub[k] = v
|
||||
logger.debug(f"Parser.sub after info scrape: {pprint.pformat(self.sub)}")
|
||||
logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}")
|
||||
|
||||
def parse_reagents(self, extraction_kit:str|None=None):
|
||||
"""
|
||||
@@ -100,6 +100,7 @@ class SheetParser(object):
|
||||
Returns:
|
||||
List[PydReagent]: List of reagents
|
||||
"""
|
||||
from frontend.widgets.pop_ups import KitSelector
|
||||
if not check_not_nan(self.sub['extraction_kit']['value']):
|
||||
dlg = KitSelector(title="Kit Needed", message="At minimum a kit is needed. Please select one.")
|
||||
if dlg.exec():
|
||||
@@ -117,7 +118,7 @@ class SheetParser(object):
|
||||
# kit = lookup_kit_types(ctx=self.ctx, name=self.sub['extraction_kit']['value'])
|
||||
kit = KitType.query(name=self.sub['extraction_kit']['value'])
|
||||
allowed_reagents = [item.name for item in kit.get_reagents()]
|
||||
logger.debug(f"List of reagents for comparison with allowed_reagents: {pprint.pformat(self.sub['reagents'])}")
|
||||
logger.debug(f"List of reagents for comparison with allowed_reagents: {pformat(self.sub['reagents'])}")
|
||||
# self.sub['reagents'] = [reagent for reagent in self.sub['reagents'] if reagent['value'].type in allowed_reagents]
|
||||
self.sub['reagents'] = [reagent for reagent in self.sub['reagents'] if reagent.type in allowed_reagents]
|
||||
|
||||
@@ -133,7 +134,7 @@ class SheetParser(object):
|
||||
Returns:
|
||||
PydSubmission: output pydantic model
|
||||
"""
|
||||
logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pprint.pformat(self.sub)}")
|
||||
logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}")
|
||||
psm = PydSubmission(filepath=self.filepath, **self.sub)
|
||||
# delattr(psm, "filepath")
|
||||
return psm
|
||||
@@ -145,7 +146,7 @@ class InfoParser(object):
|
||||
# self.ctx = ctx
|
||||
self.map = self.fetch_submission_info_map(submission_type=submission_type)
|
||||
self.xl = xl
|
||||
logger.debug(f"Info map for InfoParser: {pprint.pformat(self.map)}")
|
||||
logger.debug(f"Info map for InfoParser: {pformat(self.map)}")
|
||||
|
||||
|
||||
def fetch_submission_info_map(self, submission_type:str|dict) -> dict:
|
||||
@@ -186,7 +187,7 @@ class InfoParser(object):
|
||||
continue
|
||||
if sheet in self.map[k]['sheets']:
|
||||
relevant[k] = v
|
||||
logger.debug(f"relevant map for {sheet}: {pprint.pformat(relevant)}")
|
||||
logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
|
||||
if relevant == {}:
|
||||
continue
|
||||
for item in relevant:
|
||||
@@ -236,7 +237,7 @@ class ReagentParser(object):
|
||||
df = self.xl.parse(sheet, header=None, dtype=object)
|
||||
df.replace({np.nan: None}, inplace = True)
|
||||
relevant = {k.strip():v for k,v in self.map.items() if sheet in self.map[k]['sheet']}
|
||||
logger.debug(f"relevant map for {sheet}: {pprint.pformat(relevant)}")
|
||||
logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
|
||||
if relevant == {}:
|
||||
continue
|
||||
for item in relevant:
|
||||
@@ -302,7 +303,7 @@ class SampleParser(object):
|
||||
logger.debug(f"Looking up submission type: {submission_type}")
|
||||
# submission_type = lookup_submission_type(ctx=self.ctx, name=submission_type)
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
logger.debug(f"info_map: {pprint.pformat(submission_type.info_map)}")
|
||||
logger.debug(f"info_map: {pformat(submission_type.info_map)}")
|
||||
sample_info_map = submission_type.info_map['samples']
|
||||
# self.custom_parser = get_polymorphic_subclass(models.BasicSubmission, submission_type.name).parse_samples
|
||||
self.custom_sub_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name).parse_samples
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
from datetime import date, timedelta
|
||||
import re
|
||||
from typing import Tuple
|
||||
from tools import jinja_template_loading
|
||||
from tools import jinja_template_loading, Settings
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -198,7 +198,7 @@ def get_unique_values_in_df_column(df: DataFrame, column_name: str) -> list:
|
||||
return sorted(df[column_name].unique())
|
||||
|
||||
|
||||
def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
|
||||
def drop_reruns_from_df(ctx:Settings, df: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Removes semi-duplicates from dataframe after finding sequencing repeats.
|
||||
|
||||
@@ -211,7 +211,7 @@ def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
|
||||
"""
|
||||
if 'rerun_regex' in ctx:
|
||||
sample_names = get_unique_values_in_df_column(df, column_name="name")
|
||||
rerun_regex = re.compile(fr"{ctx['rerun_regex']}")
|
||||
rerun_regex = re.compile(fr"{ctx.rerun_regex}")
|
||||
for sample in sample_names:
|
||||
if rerun_regex.search(sample):
|
||||
first_run = re.sub(rerun_regex, "", sample)
|
||||
|
||||
@@ -10,7 +10,7 @@ class RSLNamer(object):
|
||||
"""
|
||||
Object that will enforce proper formatting on RSL plate names.
|
||||
"""
|
||||
def __init__(self, instr:str, sub_type:str|None=None):
|
||||
def __init__(self, instr:str, sub_type:str|None=None, data:dict|None=None):
|
||||
self.submission_type = sub_type
|
||||
|
||||
if self.submission_type == None:
|
||||
@@ -19,7 +19,7 @@ class RSLNamer(object):
|
||||
if self.submission_type != None:
|
||||
enforcer = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
|
||||
self.parsed_name = self.retrieve_rsl_number(instr=instr, regex=enforcer.get_regex())
|
||||
self.parsed_name = enforcer.enforce_name(instr=self.parsed_name)
|
||||
self.parsed_name = enforcer.enforce_name(instr=self.parsed_name, data=data)
|
||||
|
||||
@classmethod
|
||||
def retrieve_submission_type(cls, instr:str|Path) -> str:
|
||||
@@ -58,7 +58,7 @@ class RSLNamer(object):
|
||||
except UnboundLocalError:
|
||||
check = True
|
||||
if check:
|
||||
from frontend.custom_widgets import SubmissionTypeSelector
|
||||
from frontend.widgets import SubmissionTypeSelector
|
||||
dlg = SubmissionTypeSelector(title="Couldn't parse submission type.", message="Please select submission type from list below.")
|
||||
if dlg.exec():
|
||||
submission_type = dlg.parse_form()
|
||||
|
||||
@@ -11,7 +11,7 @@ from . import RSLNamer
|
||||
from pathlib import Path
|
||||
import re
|
||||
import logging
|
||||
from tools import check_not_nan, convert_nans_to_nones, jinja_template_loading
|
||||
from tools import check_not_nan, convert_nans_to_nones, jinja_template_loading, Report, Result
|
||||
from backend.db.models import *
|
||||
from sqlalchemy.exc import StatementError
|
||||
from PyQt6.QtWidgets import QComboBox, QWidget
|
||||
@@ -86,8 +86,8 @@ class PydReagent(BaseModel):
|
||||
else:
|
||||
return values.data['type']
|
||||
|
||||
def toSQL(self) -> Tuple[Reagent, dict]:
|
||||
result = None
|
||||
def toSQL(self) -> Tuple[Reagent, Report]:
|
||||
report = Report()
|
||||
logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
|
||||
# reagent = lookup_reagents(ctx=self.ctx, lot_number=self.lot)
|
||||
reagent = Reagent.query(lot_number=self.lot)
|
||||
@@ -113,10 +113,10 @@ class PydReagent(BaseModel):
|
||||
reagent.name = value
|
||||
# add end-of-life extension from reagent type to expiry date
|
||||
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
|
||||
return reagent, result
|
||||
return reagent, report
|
||||
|
||||
def toForm(self, parent:QWidget, extraction_kit:str) -> QComboBox:
|
||||
from frontend.custom_widgets.misc import ReagentFormWidget
|
||||
from frontend.widgets.submission_widget import ReagentFormWidget
|
||||
return ReagentFormWidget(parent=parent, reagent=self, extraction_kit=extraction_kit)
|
||||
|
||||
class PydSample(BaseModel, extra='allow'):
|
||||
@@ -180,8 +180,9 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
submission_type: dict|None
|
||||
# For defaults
|
||||
submitter_plate_num: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
||||
rsl_plate_num: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
||||
submitted_date: dict|None
|
||||
rsl_plate_num: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
|
||||
# submitted_date: dict|None
|
||||
submitting_lab: dict|None
|
||||
sample_count: dict|None
|
||||
extraction_kit: dict|None
|
||||
@@ -243,7 +244,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
@field_validator("rsl_plate_num")
|
||||
@classmethod
|
||||
def rsl_from_file(cls, value, values):
|
||||
logger.debug(f"RSL-plate initial value: {value['value']}")
|
||||
logger.debug(f"RSL-plate initial value: {value['value']} and other values: {values.data}")
|
||||
sub_type = values.data['submission_type']['value']
|
||||
if check_not_nan(value['value']):
|
||||
# if lookup_submissions(ctx=values.data['ctx'], rsl_number=value['value']) == None:
|
||||
@@ -256,7 +257,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
# return dict(value=output, missing=True)
|
||||
return value
|
||||
else:
|
||||
output = RSLNamer(instr=values.data['filepath'].__str__(), sub_type=sub_type).parsed_name
|
||||
output = RSLNamer(instr=values.data['filepath'].__str__(), sub_type=sub_type, data=values.data).parsed_name
|
||||
return dict(value=output, missing=True)
|
||||
|
||||
@field_validator("technician", mode="before")
|
||||
@@ -346,12 +347,11 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
missing_reagents = [reagent for reagent in self.reagents if reagent.missing]
|
||||
return missing_info, missing_reagents
|
||||
|
||||
def toSQL(self):
|
||||
code = 0
|
||||
msg = None
|
||||
status = None
|
||||
def toSQL(self) -> Tuple[BasicSubmission, Result]:
|
||||
|
||||
self.__dict__.update(self.model_extra)
|
||||
instance, code, msg = BasicSubmission.query_or_create(submission_type=self.submission_type['value'], rsl_plate_num=self.rsl_plate_num['value'])
|
||||
result = Result(msg=msg, code=code)
|
||||
self.handle_duplicate_samples()
|
||||
logger.debug(f"Here's our list of duplicate removed samples: {self.samples}")
|
||||
for key, value in self.__dict__.items():
|
||||
@@ -389,10 +389,10 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
except AttributeError as e:
|
||||
logger.debug(f"Something went wrong constructing instance {self.rsl_plate_num}: {e}")
|
||||
logger.debug(f"Constructed submissions message: {msg}")
|
||||
return instance, {'code':code, 'message':msg, 'status':"Information"}
|
||||
return instance, result
|
||||
|
||||
def toForm(self, parent:QWidget):
|
||||
from frontend.custom_widgets.misc import SubmissionFormWidget
|
||||
from frontend.widgets.submission_widget import SubmissionFormWidget
|
||||
return SubmissionFormWidget(parent=parent, **self.improved_dict())
|
||||
|
||||
def autofill_excel(self, missing_only:bool=True):
|
||||
|
||||
@@ -1,440 +1,5 @@
|
||||
'''
|
||||
Constructs main application.
|
||||
'''
|
||||
import sys
|
||||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QToolBar,
|
||||
QTabWidget, QWidget, QVBoxLayout,
|
||||
QComboBox, QHBoxLayout,
|
||||
QScrollArea
|
||||
)
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from pathlib import Path
|
||||
from backend.db.models import ControlType, Control
|
||||
from backend.validators import PydSubmission, PydReagent
|
||||
from .functions import (
|
||||
import_submission_function, kit_reload_function, kit_integrity_completion_function,
|
||||
submit_new_sample_function, generate_report_function, add_kit_function, add_org_function,
|
||||
controls_getter_function, chart_maker_function, link_controls_function, link_extractions_function,
|
||||
link_pcr_function, autofill_excel, scrape_reagents, export_csv_function, import_pcr_results_function
|
||||
)
|
||||
from tools import check_if_app, Settings, Report
|
||||
from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker
|
||||
import logging
|
||||
from datetime import date
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
logger = logging.getLogger(f'submissions.{__name__}')
|
||||
logger.info("Hello, I am a logger")
|
||||
|
||||
class App(QMainWindow):
|
||||
|
||||
def __init__(self, ctx: Settings = {}):
|
||||
logger.debug(f"Initializing main window...")
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.last_dir = ctx.directory_path
|
||||
self.report = Report()
|
||||
# indicate version and connected database in title bar
|
||||
try:
|
||||
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}"
|
||||
except (AttributeError, KeyError):
|
||||
self.title = f"Submissions App"
|
||||
# set initial app position and size
|
||||
self.left = 0
|
||||
self.top = 0
|
||||
self.width = 1300
|
||||
self.height = 1000
|
||||
self.setWindowTitle(self.title)
|
||||
self.setGeometry(self.left, self.top, self.width, self.height)
|
||||
# insert tabs into main app
|
||||
self.table_widget = AddSubForm(self)
|
||||
self.setCentralWidget(self.table_widget)
|
||||
# run initial setups
|
||||
self._createActions()
|
||||
self._createMenuBar()
|
||||
self._createToolBar()
|
||||
self._connectActions()
|
||||
self._controls_getter()
|
||||
self.show()
|
||||
self.statusBar().showMessage('Ready', 5000)
|
||||
|
||||
def _createMenuBar(self):
|
||||
"""
|
||||
adds items to menu bar
|
||||
"""
|
||||
logger.debug(f"Creating menu bar...")
|
||||
menuBar = self.menuBar()
|
||||
fileMenu = menuBar.addMenu("&File")
|
||||
# Creating menus using a title
|
||||
# methodsMenu = menuBar.addMenu("&Methods")
|
||||
reportMenu = menuBar.addMenu("&Reports")
|
||||
maintenanceMenu = menuBar.addMenu("&Monthly")
|
||||
helpMenu = menuBar.addMenu("&Help")
|
||||
helpMenu.addAction(self.helpAction)
|
||||
helpMenu.addAction(self.docsAction)
|
||||
fileMenu.addAction(self.importAction)
|
||||
fileMenu.addAction(self.importPCRAction)
|
||||
# methodsMenu.addAction(self.constructFS)
|
||||
reportMenu.addAction(self.generateReportAction)
|
||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||
maintenanceMenu.addAction(self.joinPCRAction)
|
||||
|
||||
def _createToolBar(self):
|
||||
"""
|
||||
adds items to toolbar
|
||||
"""
|
||||
logger.debug(f"Creating toolbar...")
|
||||
toolbar = QToolBar("My main toolbar")
|
||||
self.addToolBar(toolbar)
|
||||
toolbar.addAction(self.addReagentAction)
|
||||
toolbar.addAction(self.addKitAction)
|
||||
toolbar.addAction(self.addOrgAction)
|
||||
|
||||
def _createActions(self):
|
||||
"""
|
||||
creates actions
|
||||
"""
|
||||
logger.debug(f"Creating actions...")
|
||||
self.importAction = QAction("&Import Submission", self)
|
||||
self.importPCRAction = QAction("&Import PCR Results", self)
|
||||
self.addReagentAction = QAction("Add Reagent", self)
|
||||
self.generateReportAction = QAction("Make Report", self)
|
||||
self.addKitAction = QAction("Import Kit", self)
|
||||
self.addOrgAction = QAction("Import Org", self)
|
||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||
self.joinPCRAction = QAction("Link PCR Logs")
|
||||
self.helpAction = QAction("&About", self)
|
||||
self.docsAction = QAction("&Docs", self)
|
||||
# self.constructFS = QAction("Make First Strand", self)
|
||||
|
||||
def _connectActions(self):
|
||||
"""
|
||||
connect menu and tool bar item to functions
|
||||
"""
|
||||
logger.debug(f"Connecting actions...")
|
||||
self.importAction.triggered.connect(self.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)
|
||||
self.addOrgAction.triggered.connect(self.add_org)
|
||||
self.table_widget.control_typer.currentIndexChanged.connect(self._controls_getter)
|
||||
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.joinExtractionAction.triggered.connect(self.linkExtractions)
|
||||
self.joinPCRAction.triggered.connect(self.linkPCR)
|
||||
self.helpAction.triggered.connect(self.showAbout)
|
||||
self.docsAction.triggered.connect(self.openDocs)
|
||||
# self.constructFS.triggered.connect(self.construct_first_strand)
|
||||
self.table_widget.formwidget.import_drag.connect(self.importSubmission)
|
||||
|
||||
def showAbout(self):
|
||||
"""
|
||||
Show the 'about' message
|
||||
"""
|
||||
output = f"Version: {self.ctx.package.__version__}\n\nAuthor: {self.ctx.package.__author__['name']} - {self.ctx.package.__author__['email']}\n\nCopyright: {self.ctx.package.__copyright__}"
|
||||
about = AlertPop(message=output, status="information")
|
||||
about.exec()
|
||||
|
||||
def openDocs(self):
|
||||
"""
|
||||
Open the documentation html pages
|
||||
"""
|
||||
if check_if_app():
|
||||
url = Path(sys._MEIPASS).joinpath("files", "docs", "index.html")
|
||||
else:
|
||||
url = Path("docs\\build\\index.html").absolute()
|
||||
logger.debug(f"Attempting to open {url}")
|
||||
webbrowser.get('windows-default').open(f"file://{url.__str__()}")
|
||||
|
||||
def result_reporter(self):
|
||||
# def result_reporter(self, result:TypedDict[]|None=None):
|
||||
"""
|
||||
Report any anomolous results - if any - to the user
|
||||
|
||||
Args:
|
||||
result (dict | None, optional): The result from a function. Defaults to None.
|
||||
"""
|
||||
# logger.info(f"We got the result: {result}")
|
||||
# if result != None:
|
||||
# msg = AlertPop(message=result['message'], status=result['status'])
|
||||
# msg.exec()
|
||||
logger.debug(f"Running results reporter for: {self.report.results}")
|
||||
if len(self.report.results) > 0:
|
||||
logger.debug(f"We've got some results!")
|
||||
for result in self.report.results:
|
||||
logger.debug(f"Showing result: {result}")
|
||||
if result != None:
|
||||
alert = result.report()
|
||||
if alert.exec():
|
||||
pass
|
||||
self.report = Report()
|
||||
else:
|
||||
self.statusBar().showMessage("Action completed sucessfully.", 5000)
|
||||
|
||||
def importSubmission(self, fname:Path|None=None):
|
||||
"""
|
||||
import submission from excel sheet into form
|
||||
"""
|
||||
# from .main_window_functions import import_submission_function
|
||||
self.raise_()
|
||||
self.activateWindow()
|
||||
self = import_submission_function(self, fname)
|
||||
logger.debug(f"Result from result reporter: {self.report.results}")
|
||||
self.result_reporter()
|
||||
|
||||
def kit_reload(self):
|
||||
"""
|
||||
Removes all reagents from form before running kit integrity completion.
|
||||
"""
|
||||
# from .main_window_functions import kit_reload_function
|
||||
self = kit_reload_function(self)
|
||||
self.result_reporter()
|
||||
|
||||
def kit_integrity_completion(self):
|
||||
"""
|
||||
Performs check of imported reagents
|
||||
NOTE: this will not change self.reagents which should be fine
|
||||
since it's only used when looking up
|
||||
"""
|
||||
# from .main_window_functions import kit_integrity_completion_function
|
||||
self = kit_integrity_completion_function(self)
|
||||
self.result_reporter()
|
||||
|
||||
def submit_new_sample(self):
|
||||
"""
|
||||
Attempt to add sample to database when 'submit' button clicked
|
||||
"""
|
||||
# from .main_window_functions import submit_new_sample_function
|
||||
self, result = submit_new_sample_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, name:str|None=None):
|
||||
"""
|
||||
Action to create new reagent in DB.
|
||||
|
||||
Args:
|
||||
reagent_lot (str | None, optional): Parsed reagent from import form. Defaults to None.
|
||||
reagent_type (str | None, optional): Parsed reagent type from import form. Defaults to None.
|
||||
|
||||
Returns:
|
||||
models.Reagent: the constructed reagent object to add to submission
|
||||
"""
|
||||
if isinstance(reagent_lot, bool):
|
||||
reagent_lot = ""
|
||||
# create form
|
||||
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
|
||||
if dlg.exec():
|
||||
# extract form info
|
||||
# info = extract_form_info(dlg)
|
||||
info = dlg.parse_form()
|
||||
logger.debug(f"Reagent info: {info}")
|
||||
# create reagent object
|
||||
# reagent = construct_reagent(ctx=self.ctx, info_dict=info)
|
||||
reagent = PydReagent(ctx=self.ctx, **info)
|
||||
# send reagent to db
|
||||
# store_reagent(ctx=self.ctx, reagent=reagent)
|
||||
sqlobj, result = reagent.toSQL()
|
||||
sqlobj.save()
|
||||
# result = store_object(ctx=self.ctx, object=reagent.toSQL()[0])
|
||||
self.result_reporter(result=result)
|
||||
return reagent
|
||||
|
||||
def generateReport(self):
|
||||
"""
|
||||
Action to create a summary of sheet data per client
|
||||
"""
|
||||
# from .main_window_functions import generate_report_function
|
||||
self, result = generate_report_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def add_kit(self):
|
||||
"""
|
||||
Constructs new kit from yaml and adds to DB.
|
||||
"""
|
||||
# from .main_window_functions import add_kit_function
|
||||
self, result = add_kit_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def add_org(self):
|
||||
"""
|
||||
Constructs new kit from yaml and adds to DB.
|
||||
"""
|
||||
# from .main_window_functions import add_org_function
|
||||
self, result = add_org_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def _controls_getter(self):
|
||||
"""
|
||||
Lookup controls from database and send to chartmaker
|
||||
"""
|
||||
# from .main_window_functions import controls_getter_function
|
||||
self = controls_getter_function(self)
|
||||
self.result_reporter()
|
||||
|
||||
def _chart_maker(self):
|
||||
"""
|
||||
Creates plotly charts for webview
|
||||
"""
|
||||
# from .main_window_functions import chart_maker_function
|
||||
self = chart_maker_function(self)
|
||||
self.result_reporter()
|
||||
|
||||
def linkControls(self):
|
||||
"""
|
||||
Adds controls pulled from irida to relevant submissions
|
||||
NOTE: Depreciated due to improvements in controls scraper.
|
||||
"""
|
||||
# from .main_window_functions import link_controls_function
|
||||
self, result = link_controls_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def linkExtractions(self):
|
||||
"""
|
||||
Links extraction logs from .csv files to relevant submissions.
|
||||
"""
|
||||
# from .main_window_functions import link_extractions_function
|
||||
self, result = link_extractions_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def linkPCR(self):
|
||||
"""
|
||||
Links PCR logs from .csv files to relevant submissions.
|
||||
"""
|
||||
# from .main_window_functions import link_pcr_function
|
||||
self, result = link_pcr_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
def importPCRResults(self):
|
||||
"""
|
||||
Imports results exported from Design and Analysis .eds files
|
||||
"""
|
||||
# from .main_window_functions import import_pcr_results_function
|
||||
self, result = import_pcr_results_function(self)
|
||||
self.result_reporter(result)
|
||||
|
||||
# def construct_first_strand(self):
|
||||
# """
|
||||
# Converts first strand excel sheet to Biomek CSV
|
||||
# """
|
||||
# from .main_window_functions import construct_first_strand_function
|
||||
# self, result = construct_first_strand_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
def scrape_reagents(self, *args, **kwargs):
|
||||
# from .main_window_functions import scrape_reagents
|
||||
logger.debug(f"Args: {args}")
|
||||
logger.debug(F"kwargs: {kwargs}")
|
||||
self = scrape_reagents(self, args[0])
|
||||
self.kit_integrity_completion()
|
||||
self.result_reporter()
|
||||
|
||||
def export_csv(self, fname:Path|None=None):
|
||||
from .main_window_functions import export_csv_function
|
||||
export_csv_function(self, fname)
|
||||
|
||||
class AddSubForm(QWidget):
|
||||
|
||||
def __init__(self, parent:QWidget):
|
||||
logger.debug(f"Initializating subform...")
|
||||
super(QWidget, self).__init__(parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.parent = parent
|
||||
# Initialize tab screen
|
||||
self.tabs = QTabWidget()
|
||||
self.tab1 = QWidget()
|
||||
self.tab2 = QWidget()
|
||||
self.tab3 = QWidget()
|
||||
self.tabs.resize(300,200)
|
||||
|
||||
# Add tabs
|
||||
self.tabs.addTab(self.tab1,"Submissions")
|
||||
self.tabs.addTab(self.tab2,"Controls")
|
||||
self.tabs.addTab(self.tab3, "Add Kit")
|
||||
# Create submission adder form
|
||||
self.formwidget = SubmissionFormContainer(self)
|
||||
self.formlayout = QVBoxLayout(self)
|
||||
self.formwidget.setLayout(self.formlayout)
|
||||
self.formwidget.setFixedWidth(300)
|
||||
# Make scrollable interior for form
|
||||
self.interior = QScrollArea(self.tab1)
|
||||
self.interior.setWidgetResizable(True)
|
||||
self.interior.setFixedWidth(325)
|
||||
self.interior.setWidget(self.formwidget)
|
||||
# Create sheet to hold existing submissions
|
||||
self.sheetwidget = QWidget(self)
|
||||
self.sheetlayout = QVBoxLayout(self)
|
||||
self.sheetwidget.setLayout(self.sheetlayout)
|
||||
self.sub_wid = SubmissionsSheet(parent.ctx)
|
||||
self.sheetlayout.addWidget(self.sub_wid)
|
||||
# Create layout of first tab to hold form and sheet
|
||||
self.tab1.layout = QHBoxLayout(self)
|
||||
self.tab1.setLayout(self.tab1.layout)
|
||||
self.tab1.layout.addWidget(self.interior)
|
||||
self.tab1.layout.addWidget(self.sheetwidget)
|
||||
# create widgets for tab 2
|
||||
self.datepicker = ControlsDatePicker()
|
||||
self.webengineview = QWebEngineView()
|
||||
# set tab2 layout
|
||||
self.tab2.layout = QVBoxLayout(self)
|
||||
self.control_typer = QComboBox()
|
||||
# fetch types of controls
|
||||
# con_types = get_all_Control_Types_names(ctx=parent.ctx)
|
||||
# con_types = [item.name for item in lookup_control_types(ctx=parent.ctx)]
|
||||
con_types = [item.name for item in ControlType.query()]
|
||||
self.control_typer.addItems(con_types)
|
||||
# create custom widget to get types of analysis
|
||||
self.mode_typer = QComboBox()
|
||||
# mode_types = get_all_available_modes(ctx=parent.ctx)
|
||||
# mode_types = lookup_modes(ctx=parent.ctx)
|
||||
mode_types = Control.get_modes()
|
||||
self.mode_typer.addItems(mode_types)
|
||||
# create custom widget to get subtypes of analysis
|
||||
self.sub_typer = QComboBox()
|
||||
self.sub_typer.setEnabled(False)
|
||||
# add widgets to tab2 layout
|
||||
self.tab2.layout.addWidget(self.datepicker)
|
||||
self.tab2.layout.addWidget(self.control_typer)
|
||||
self.tab2.layout.addWidget(self.mode_typer)
|
||||
self.tab2.layout.addWidget(self.sub_typer)
|
||||
self.tab2.layout.addWidget(self.webengineview)
|
||||
self.tab2.setLayout(self.tab2.layout)
|
||||
# create custom widget to add new tabs
|
||||
adder = KitAdder(parent_ctx=parent.ctx)
|
||||
self.tab3.layout = QVBoxLayout(self)
|
||||
self.tab3.layout.addWidget(adder)
|
||||
self.tab3.setLayout(self.tab3.layout)
|
||||
# add tabs to main widget
|
||||
self.layout.addWidget(self.tabs)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
class SubmissionFormContainer(QWidget):
|
||||
|
||||
import_drag = pyqtSignal(Path)
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
logger.debug(f"Setting form widget...")
|
||||
super().__init__(parent)
|
||||
# self.parent = parent
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if event.mimeData().hasUrls():
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def dropEvent(self, event):
|
||||
fname = Path([u.toLocalFile() for u in event.mimeData().urls()][0])
|
||||
app = self.parent().parent().parent().parent().parent().parent().parent
|
||||
logger.debug(f"App: {app}")
|
||||
app.last_dir = fname.parent
|
||||
self.import_drag.emit(fname)
|
||||
|
||||
from .widgets import *
|
||||
from .visualizations import *
|
||||
@@ -1,7 +0,0 @@
|
||||
'''
|
||||
Contains all custom generated PyQT6 derivative widgets.
|
||||
'''
|
||||
|
||||
from .misc import *
|
||||
from .pop_ups import *
|
||||
from .sub_details import *
|
||||
@@ -1,767 +0,0 @@
|
||||
'''
|
||||
Contains miscellaneous widgets for frontend functions
|
||||
'''
|
||||
from datetime import date
|
||||
from pprint import pformat
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout,
|
||||
QLineEdit, QComboBox, QDialog,
|
||||
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
|
||||
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
|
||||
QHBoxLayout, QScrollArea, QFormLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QDate, QSize, pyqtSignal
|
||||
from tools import check_not_nan, jinja_template_loading, Settings
|
||||
from backend.db.models import *
|
||||
from sqlalchemy import FLOAT, INTEGER
|
||||
import logging
|
||||
import numpy as np
|
||||
from .pop_ups import AlertPop, QuestionAsker
|
||||
from backend.validators import PydReagent, PydKit, PydReagentType, PydSubmission
|
||||
from typing import Tuple, List
|
||||
from pprint import pformat
|
||||
import difflib
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
env = jinja_template_loading()
|
||||
|
||||
class AddReagentForm(QDialog):
|
||||
"""
|
||||
dialog to add gather info about new reagent
|
||||
"""
|
||||
def __init__(self, ctx:dict, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, reagent_name:str|None=None) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
if reagent_lot == None:
|
||||
reagent_lot = reagent_type
|
||||
|
||||
self.setWindowTitle("Add Reagent")
|
||||
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
# widget to get lot info
|
||||
self.name_input = QComboBox()
|
||||
self.name_input.setObjectName("name")
|
||||
self.name_input.setEditable(True)
|
||||
self.name_input.setCurrentText(reagent_name)
|
||||
# self.name_input.setText(reagent_name)
|
||||
self.lot_input = QLineEdit()
|
||||
self.lot_input.setObjectName("lot")
|
||||
self.lot_input.setText(reagent_lot)
|
||||
# widget to get expiry info
|
||||
self.exp_input = QDateEdit(calendarPopup=True)
|
||||
self.exp_input.setObjectName('expiry')
|
||||
# if expiry is not passed in from gui, use today
|
||||
if expiry == None:
|
||||
self.exp_input.setDate(QDate.currentDate())
|
||||
else:
|
||||
self.exp_input.setDate(expiry)
|
||||
# widget to get reagent type info
|
||||
self.type_input = QComboBox()
|
||||
self.type_input.setObjectName('type')
|
||||
# self.type_input.addItems([item.name for item in lookup_reagent_types(ctx=ctx)])
|
||||
self.type_input.addItems([item.name for item in ReagentType.query()])
|
||||
logger.debug(f"Trying to find index of {reagent_type}")
|
||||
# convert input to user friendly string?
|
||||
try:
|
||||
reagent_type = reagent_type.replace("_", " ").title()
|
||||
except AttributeError:
|
||||
reagent_type = None
|
||||
# set parsed reagent type to top of list
|
||||
index = self.type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
|
||||
if index >= 0:
|
||||
self.type_input.setCurrentIndex(index)
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(QLabel("Name:"))
|
||||
self.layout.addWidget(self.name_input)
|
||||
self.layout.addWidget(QLabel("Lot:"))
|
||||
self.layout.addWidget(self.lot_input)
|
||||
self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
|
||||
self.layout.addWidget(self.exp_input)
|
||||
self.layout.addWidget(QLabel("Type:"))
|
||||
self.layout.addWidget(self.type_input)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
self.type_input.currentTextChanged.connect(self.update_names)
|
||||
|
||||
def parse_form(self):
|
||||
return dict(name=self.name_input.currentText(),
|
||||
lot=self.lot_input.text(),
|
||||
expiry=self.exp_input.date().toPyDate(),
|
||||
type=self.type_input.currentText())
|
||||
|
||||
def update_names(self):
|
||||
"""
|
||||
Updates reagent names form field with examples from reagent type
|
||||
"""
|
||||
logger.debug(self.type_input.currentText())
|
||||
self.name_input.clear()
|
||||
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=self.type_input.currentText())
|
||||
lookup = Reagent.query(reagent_type=self.type_input.currentText())
|
||||
self.name_input.addItems(list(set([item.name for item in lookup])))
|
||||
|
||||
class ReportDatePicker(QDialog):
|
||||
"""
|
||||
custom dialog to ask for report start/stop dates
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Select Report Date Range")
|
||||
# make confirm/reject buttons
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
# widgets to ask for dates
|
||||
self.start_date = QDateEdit(calendarPopup=True)
|
||||
self.start_date.setObjectName("start_date")
|
||||
self.start_date.setDate(QDate.currentDate())
|
||||
self.end_date = QDateEdit(calendarPopup=True)
|
||||
self.end_date.setObjectName("end_date")
|
||||
self.end_date.setDate(QDate.currentDate())
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(QLabel("Start Date"))
|
||||
self.layout.addWidget(self.start_date)
|
||||
self.layout.addWidget(QLabel("End Date"))
|
||||
self.layout.addWidget(self.end_date)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self):
|
||||
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
||||
|
||||
class KitAdder(QWidget):
|
||||
"""
|
||||
dialog to get information to add kit
|
||||
"""
|
||||
def __init__(self, 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)
|
||||
scrollContent.setLayout(self.grid)
|
||||
# insert submit button at top
|
||||
self.submit_btn = QPushButton("Submit")
|
||||
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
||||
self.grid.addWidget(QLabel("Kit Name:"),2,0)
|
||||
# widget to get kit name
|
||||
kit_name = QLineEdit()
|
||||
kit_name.setObjectName("kit_name")
|
||||
self.grid.addWidget(kit_name,2,1)
|
||||
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||
# widget to get uses of kit
|
||||
used_for = QComboBox()
|
||||
used_for.setObjectName("used_for")
|
||||
# Insert all existing sample types
|
||||
# used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
|
||||
used_for.addItems([item.name for item in SubmissionType.query()])
|
||||
used_for.setEditable(True)
|
||||
self.grid.addWidget(used_for,3,1)
|
||||
# Get all fields in SubmissionTypeKitTypeAssociation
|
||||
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
|
||||
for iii, column in enumerate(self.columns):
|
||||
idx = iii + 4
|
||||
# convert field name to human readable.
|
||||
field_name = column.name.replace("_", " ").title()
|
||||
self.grid.addWidget(QLabel(field_name),idx,0)
|
||||
match column.type:
|
||||
case FLOAT():
|
||||
add_widget = QDoubleSpinBox()
|
||||
add_widget.setMinimum(0)
|
||||
add_widget.setMaximum(9999)
|
||||
case INTEGER():
|
||||
add_widget = QSpinBox()
|
||||
add_widget.setMinimum(0)
|
||||
add_widget.setMaximum(9999)
|
||||
case _:
|
||||
add_widget = QLineEdit()
|
||||
add_widget.setObjectName(column.name)
|
||||
self.grid.addWidget(add_widget, idx,1)
|
||||
self.add_RT_btn = QPushButton("Add Reagent Type")
|
||||
self.grid.addWidget(self.add_RT_btn)
|
||||
self.add_RT_btn.clicked.connect(self.add_RT)
|
||||
self.submit_btn.clicked.connect(self.submit)
|
||||
scroll.setWidget(scrollContent)
|
||||
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
"qt_scrollarea_vcontainer", "submit_btn"
|
||||
]
|
||||
|
||||
def add_RT(self) -> None:
|
||||
"""
|
||||
insert new reagent type row
|
||||
"""
|
||||
# get bottommost row
|
||||
maxrow = self.grid.rowCount()
|
||||
reg_form = ReagentTypeForm(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,0,1,4)
|
||||
|
||||
|
||||
|
||||
def submit(self) -> None:
|
||||
"""
|
||||
send kit to database
|
||||
"""
|
||||
# get form info
|
||||
info, reagents = self.parse_form()
|
||||
# info, reagents = extract_form_info(self)
|
||||
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
|
||||
logger.debug(f"kit info: {pformat(info)}")
|
||||
logger.debug(f"kit reagents: {pformat(reagents)}")
|
||||
info['reagent_types'] = reagents
|
||||
logger.debug(pformat(info))
|
||||
# send to kit constructor
|
||||
kit = PydKit(name=info['kit_name'])
|
||||
for reagent in info['reagent_types']:
|
||||
uses = {
|
||||
info['used_for']:
|
||||
{'sheet':reagent['sheet'],
|
||||
'name':reagent['name'],
|
||||
'lot':reagent['lot'],
|
||||
'expiry':reagent['expiry']
|
||||
}}
|
||||
kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
|
||||
logger.debug(f"Output pyd object: {kit.__dict__}")
|
||||
# result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
|
||||
sqlobj, result = kit.toSQL(self.ctx)
|
||||
sqlobj.save()
|
||||
msg = AlertPop(message=result['message'], status=result['status'])
|
||||
msg.exec()
|
||||
self.__init__(self.ctx)
|
||||
|
||||
def parse_form(self) -> Tuple[dict, list]:
|
||||
logger.debug(f"Hello from {self.__class__} parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
|
||||
for widget in widgets:
|
||||
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||
match widget:
|
||||
case ReagentTypeForm():
|
||||
reagents.append(widget.parse_form())
|
||||
case QLineEdit():
|
||||
info[widget.objectName()] = widget.text()
|
||||
case QComboBox():
|
||||
info[widget.objectName()] = widget.currentText()
|
||||
case QDateEdit():
|
||||
info[widget.objectName()] = widget.date().toPyDate()
|
||||
return info, reagents
|
||||
|
||||
class ReagentTypeForm(QWidget):
|
||||
"""
|
||||
custom widget to add information about a new reagenttype
|
||||
"""
|
||||
def __init__(self, ctx:Settings) -> None:
|
||||
super().__init__()
|
||||
grid = QGridLayout()
|
||||
self.setLayout(grid)
|
||||
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||
# Widget to get reagent info
|
||||
self.reagent_getter = QComboBox()
|
||||
self.reagent_getter.setObjectName("rtname")
|
||||
# lookup all reagent type names from db
|
||||
# lookup = lookup_reagent_types(ctx=ctx)
|
||||
lookup = ReagentType.query()
|
||||
logger.debug(f"Looked up ReagentType names: {lookup}")
|
||||
self.reagent_getter.addItems([item.__str__() for item in lookup])
|
||||
self.reagent_getter.setEditable(True)
|
||||
grid.addWidget(self.reagent_getter,0,1)
|
||||
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
|
||||
# widget to get extension of life
|
||||
self.eol = QSpinBox()
|
||||
self.eol.setObjectName('eol')
|
||||
self.eol.setMinimum(0)
|
||||
grid.addWidget(self.eol, 0,3)
|
||||
grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
|
||||
self.location_sheet_name = QLineEdit()
|
||||
self.location_sheet_name.setObjectName("sheet")
|
||||
self.location_sheet_name.setText("e.g. 'Reagent Info'")
|
||||
grid.addWidget(self.location_sheet_name, 1,1)
|
||||
for iii, item in enumerate(["Name", "Lot", "Expiry"]):
|
||||
idx = iii + 2
|
||||
grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
|
||||
row = QSpinBox()
|
||||
row.setFixedWidth(50)
|
||||
row.setObjectName(f'{item.lower()}_row')
|
||||
row.setMinimum(0)
|
||||
grid.addWidget(row, idx, 1)
|
||||
grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
|
||||
col = QSpinBox()
|
||||
col.setFixedWidth(50)
|
||||
col.setObjectName(f'{item.lower()}_column')
|
||||
col.setMinimum(0)
|
||||
grid.addWidget(col, idx, 3)
|
||||
self.setFixedHeight(175)
|
||||
max_row = grid.rowCount()
|
||||
self.r_button = QPushButton("Remove")
|
||||
self.r_button.clicked.connect(self.remove)
|
||||
grid.addWidget(self.r_button,max_row,0,1,1)
|
||||
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
"qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
|
||||
]
|
||||
|
||||
def remove(self):
|
||||
self.setParent(None)
|
||||
self.destroy()
|
||||
|
||||
def parse_form(self) -> dict:
|
||||
logger.debug(f"Hello from {self.__class__} parser!")
|
||||
info = {}
|
||||
info['eol'] = self.eol.value()
|
||||
info['sheet'] = self.location_sheet_name.text()
|
||||
info['rtname'] = self.reagent_getter.currentText()
|
||||
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
|
||||
for widget in widgets:
|
||||
logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||
match widget:
|
||||
case QLineEdit():
|
||||
info[widget.objectName()] = widget.text()
|
||||
case QComboBox():
|
||||
info[widget.objectName()] = widget.currentText()
|
||||
case QDateEdit():
|
||||
info[widget.objectName()] = widget.date().toPyDate()
|
||||
case QSpinBox() | QDoubleSpinBox():
|
||||
if "_" in widget.objectName():
|
||||
key, sub_key = widget.objectName().split("_")
|
||||
if key not in info.keys():
|
||||
info[key] = {}
|
||||
logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
|
||||
info[key][sub_key] = widget.value()
|
||||
return info
|
||||
|
||||
class ControlsDatePicker(QWidget):
|
||||
"""
|
||||
custom widget to pick start and end dates for controls graphs
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.start_date = QDateEdit(calendarPopup=True)
|
||||
# start date is two months prior to end date by default
|
||||
twomonthsago = QDate.currentDate().addDays(-60)
|
||||
self.start_date.setDate(twomonthsago)
|
||||
self.end_date = QDateEdit(calendarPopup=True)
|
||||
self.end_date.setDate(QDate.currentDate())
|
||||
self.layout = QHBoxLayout()
|
||||
self.layout.addWidget(QLabel("Start Date"))
|
||||
self.layout.addWidget(self.start_date)
|
||||
self.layout.addWidget(QLabel("End Date"))
|
||||
self.layout.addWidget(self.end_date)
|
||||
self.setLayout(self.layout)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return QSize(80,20)
|
||||
|
||||
class FirstStrandSalvage(QDialog):
|
||||
|
||||
def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None:
|
||||
super().__init__()
|
||||
if rsl_plate_num == None:
|
||||
rsl_plate_num = ""
|
||||
self.setWindowTitle("Add Reagent")
|
||||
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
self.submitter_id_input = QLineEdit()
|
||||
self.submitter_id_input.setText(submitter_id)
|
||||
self.rsl_plate_num = QLineEdit()
|
||||
self.rsl_plate_num.setText(rsl_plate_num)
|
||||
self.row_letter = QComboBox()
|
||||
self.row_letter.addItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
|
||||
self.row_letter.setEditable(False)
|
||||
self.column_number = QComboBox()
|
||||
self.column_number.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'])
|
||||
self.column_number.setEditable(False)
|
||||
self.layout = QFormLayout()
|
||||
self.layout.addRow(self.tr("&Sample Number:"), self.submitter_id_input)
|
||||
self.layout.addRow(self.tr("&Plate Number:"), self.rsl_plate_num)
|
||||
self.layout.addRow(self.tr("&Source Row:"), self.row_letter)
|
||||
self.layout.addRow(self.tr("&Source Column:"), self.column_number)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self):
|
||||
return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}")
|
||||
|
||||
class FirstStrandPlateList(QDialog):
|
||||
|
||||
def __init__(self, ctx:Settings) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle("First Strand Plates")
|
||||
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
# ww = [item.rsl_plate_num for item in lookup_submissions(ctx=ctx, submission_type="Wastewater")]
|
||||
ww = [item.rsl_plate_num for item in BasicSubmission.query(submission_type="Wastewater")]
|
||||
self.plate1 = QComboBox()
|
||||
self.plate2 = QComboBox()
|
||||
self.plate3 = QComboBox()
|
||||
self.layout = QFormLayout()
|
||||
for ii, plate in enumerate([self.plate1, self.plate2, self.plate3]):
|
||||
plate.addItems(ww)
|
||||
self.layout.addRow(self.tr(f"&Plate {ii+1}:"), plate)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self):
|
||||
output = []
|
||||
for plate in [self.plate1, self.plate2, self.plate3]:
|
||||
output.append(plate.currentText())
|
||||
return output
|
||||
|
||||
class ReagentFormWidget(QWidget):
|
||||
|
||||
def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
|
||||
super().__init__(parent)
|
||||
# self.setParent(parent)
|
||||
self.reagent = reagent
|
||||
self.extraction_kit = extraction_kit
|
||||
# self.ctx = reagent.ctx
|
||||
layout = QVBoxLayout()
|
||||
self.label = self.ReagentParsedLabel(reagent=reagent)
|
||||
layout.addWidget(self.label)
|
||||
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
||||
layout.addWidget(self.lot)
|
||||
# Remove spacing between reagents
|
||||
layout.setContentsMargins(0,0,0,0)
|
||||
self.setLayout(layout)
|
||||
self.setObjectName(reagent.name)
|
||||
self.missing = reagent.missing
|
||||
# If changed set self.missing to True and update self.label
|
||||
self.lot.currentTextChanged.connect(self.updated)
|
||||
|
||||
def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||
lot = self.lot.currentText()
|
||||
# wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
|
||||
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
||||
# if reagent doesn't exist in database, off to add it (uses App.add_reagent)
|
||||
if wanted_reagent == None:
|
||||
dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
|
||||
if dlg.exec():
|
||||
wanted_reagent = self.parent().parent().parent().parent().parent().parent().parent().parent().parent.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
|
||||
return wanted_reagent, None
|
||||
else:
|
||||
# In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||
logger.debug("Will not add reagent.")
|
||||
return None, dict(message="Failed integrity check", status="critical")
|
||||
else:
|
||||
# Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
||||
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
||||
# rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
|
||||
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
rt = ReagentType.query(name=self.reagent.type)
|
||||
if rt == None:
|
||||
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
|
||||
|
||||
def updated(self):
|
||||
self.missing = True
|
||||
self.label.updated(self.reagent.type)
|
||||
|
||||
|
||||
class ReagentParsedLabel(QLabel):
|
||||
|
||||
def __init__(self, reagent:PydReagent):
|
||||
super().__init__()
|
||||
try:
|
||||
check = not reagent.missing
|
||||
except:
|
||||
check = False
|
||||
self.setObjectName(f"{reagent.type}_label")
|
||||
if check:
|
||||
self.setText(f"Parsed {reagent.type}")
|
||||
else:
|
||||
self.setText(f"MISSING {reagent.type}")
|
||||
|
||||
def updated(self, reagent_type:str):
|
||||
self.setText(f"UPDATED {reagent_type}")
|
||||
|
||||
class ReagentLot(QComboBox):
|
||||
|
||||
def __init__(self, reagent, extraction_kit:str) -> None:
|
||||
super().__init__()
|
||||
# self.ctx = reagent.ctx
|
||||
self.setEditable(True)
|
||||
# if reagent.parsed:
|
||||
# pass
|
||||
logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
|
||||
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
|
||||
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type)
|
||||
lookup = Reagent.query(reagent_type=reagent.type)
|
||||
relevant_reagents = [item.__str__() for item in lookup]
|
||||
output_reg = []
|
||||
for rel_reagent in relevant_reagents:
|
||||
# extract strings from any sets.
|
||||
if isinstance(rel_reagent, set):
|
||||
for thing in rel_reagent:
|
||||
output_reg.append(thing)
|
||||
elif isinstance(rel_reagent, str):
|
||||
output_reg.append(rel_reagent)
|
||||
relevant_reagents = output_reg
|
||||
# if reagent in sheet is not found insert it into the front of relevant reagents so it shows
|
||||
logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}")
|
||||
if str(reagent.lot) not in relevant_reagents:
|
||||
if check_not_nan(reagent.lot):
|
||||
relevant_reagents.insert(0, str(reagent.lot))
|
||||
else:
|
||||
# TODO: look up the last used reagent of this type in the database
|
||||
# looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
|
||||
looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
|
||||
try:
|
||||
# looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
|
||||
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
||||
except AttributeError:
|
||||
looked_up_reg = None
|
||||
logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
|
||||
if looked_up_reg != None:
|
||||
relevant_reagents.remove(str(looked_up_reg.lot))
|
||||
relevant_reagents.insert(0, str(looked_up_reg.lot))
|
||||
else:
|
||||
if len(relevant_reagents) > 1:
|
||||
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
||||
idx = relevant_reagents.index(str(reagent.lot))
|
||||
logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}")
|
||||
moved_reag = relevant_reagents.pop(idx)
|
||||
relevant_reagents.insert(0, moved_reag)
|
||||
else:
|
||||
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
|
||||
logger.debug(f"New relevant reagents: {relevant_reagents}")
|
||||
self.setObjectName(f"lot_{reagent.type}")
|
||||
self.addItems(relevant_reagents)
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, **kwargs) -> None:
|
||||
super().__init__(parent)
|
||||
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
# "qt_scrollarea_vcontainer", "submit_btn"
|
||||
# ]
|
||||
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx']
|
||||
layout = QVBoxLayout()
|
||||
for k, v in kwargs.items():
|
||||
if k not in self.ignore:
|
||||
add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type'])
|
||||
if add_widget != None:
|
||||
layout.addWidget(add_widget)
|
||||
else:
|
||||
setattr(self, k, v)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_widget(self, key:str, value:dict, submission_type:str|None=None):
|
||||
if key not in self.ignore:
|
||||
return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
|
||||
return None
|
||||
|
||||
def clear_form(self):
|
||||
for item in self.findChildren(QWidget):
|
||||
item.setParent(None)
|
||||
|
||||
def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
|
||||
query = self.findChildren(QWidget)
|
||||
if object_name != None:
|
||||
query = [widget for widget in query if widget.objectName()==object_name]
|
||||
return query
|
||||
|
||||
def parse_form(self) -> PydSubmission:
|
||||
logger.debug(f"Hello from form parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
if hasattr(self, 'csv'):
|
||||
info['csv'] = self.csv
|
||||
# samples = self.parent().parent.parent.samples
|
||||
# filepath = self.parent().parent.parent.pyd.filepath
|
||||
# logger.debug(f"Using samples: {pformat(samples)}")
|
||||
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
|
||||
# widgets = [widget for widget in self.findChildren(QWidget)]
|
||||
# for widget in widgets:
|
||||
for widget in self.findChildren(QWidget):
|
||||
logger.debug(f"Parsed widget of type {type(widget)}")
|
||||
match widget:
|
||||
case ReagentFormWidget():
|
||||
reagent, _ = widget.parse_form()
|
||||
reagents.append(reagent)
|
||||
case self.InfoItem():
|
||||
field, value = widget.parse_form()
|
||||
if field != None:
|
||||
info[field] = value
|
||||
# case ImportReagent():
|
||||
# reagent = dict(name=widget.objectName().replace("lot_", ""), lot=widget.currentText(), type=None, expiry=None)
|
||||
# # ctx: self.SubmissionContinerWidget.AddSubForm
|
||||
# reagents.append(PydReagent(ctx=self.parent.parent.ctx, **reagent))
|
||||
# case QLineEdit():
|
||||
# info[widget.objectName()] = dict(value=widget.text())
|
||||
# case QComboBox():
|
||||
# info[widget.objectName()] = dict(value=widget.currentText())
|
||||
# case QDateEdit():
|
||||
# info[widget.objectName()] = dict(value=widget.date().toPyDate())
|
||||
logger.debug(f"Info: {pformat(info)}")
|
||||
logger.debug(f"Reagents: {pformat(reagents)}")
|
||||
app = self.parent().parent().parent().parent().parent().parent().parent().parent
|
||||
submission = PydSubmission(ctx=app.ctx, filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
|
||||
return submission
|
||||
|
||||
class InfoItem(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> None:
|
||||
super().__init__(parent)
|
||||
layout = QVBoxLayout()
|
||||
self.label = self.ParsedQLabel(key=key, value=value)
|
||||
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type['value'])
|
||||
self.setObjectName(key)
|
||||
try:
|
||||
self.missing:bool = value['missing']
|
||||
except (TypeError, KeyError):
|
||||
self.missing:bool = False
|
||||
if self.input != None:
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.input)
|
||||
layout.setContentsMargins(0,0,0,0)
|
||||
self.setLayout(layout)
|
||||
match self.input:
|
||||
case QComboBox():
|
||||
self.input.currentTextChanged.connect(self.update_missing)
|
||||
case QDateEdit():
|
||||
self.input.dateChanged.connect(self.update_missing)
|
||||
case QLineEdit():
|
||||
self.input.textChanged.connect(self.update_missing)
|
||||
|
||||
def parse_form(self):
|
||||
match self.input:
|
||||
case QLineEdit():
|
||||
value = self.input.text()
|
||||
case QComboBox():
|
||||
value = self.input.currentText()
|
||||
case QDateEdit():
|
||||
value = self.input.date().toPyDate()
|
||||
case _:
|
||||
return None, None
|
||||
return self.input.objectName(), dict(value=value, missing=self.missing)
|
||||
|
||||
def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
|
||||
try:
|
||||
value = value['value']
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
obj = parent.parent().parent()
|
||||
logger.debug(f"Creating widget for: {key}")
|
||||
match key:
|
||||
case 'submitting_lab':
|
||||
add_widget = QComboBox()
|
||||
# lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
# labs = [item.__str__() for item in lookup_organizations(ctx=obj.ctx)]
|
||||
labs = [item.__str__() for item in Organization.query()]
|
||||
# try to set closest match to top of list
|
||||
try:
|
||||
labs = difflib.get_close_matches(value, labs, len(labs), 0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
# set combobox values to lookedup values
|
||||
add_widget.addItems(labs)
|
||||
case 'extraction_kit':
|
||||
# if extraction kit not available, all other values fail
|
||||
if not check_not_nan(value):
|
||||
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
|
||||
msg.exec()
|
||||
# create combobox to hold looked up kits
|
||||
add_widget = QComboBox()
|
||||
# lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
logger.debug(f"Looking up kits used for {submission_type}")
|
||||
# uses = [item.__str__() for item in lookup_kit_types(ctx=obj.ctx, used_for=submission_type)]
|
||||
uses = [item.__str__() for item in KitType.query(used_for=submission_type)]
|
||||
obj.uses = uses
|
||||
logger.debug(f"Kits received for {submission_type}: {uses}")
|
||||
if check_not_nan(value):
|
||||
logger.debug(f"The extraction kit in parser was: {value}")
|
||||
uses.insert(0, uses.pop(uses.index(value)))
|
||||
obj.ext_kit = value
|
||||
else:
|
||||
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
||||
obj.ext_kit = uses[0]
|
||||
add_widget.addItems(uses)
|
||||
|
||||
# Run reagent scraper whenever extraction kit is changed.
|
||||
# add_widget.currentTextChanged.connect(obj.scrape_reagents)
|
||||
case 'submitted_date':
|
||||
# uses base calendar
|
||||
add_widget = QDateEdit(calendarPopup=True)
|
||||
# sets submitted date based on date found in excel sheet
|
||||
try:
|
||||
add_widget.setDate(value)
|
||||
# if not found, use today
|
||||
except:
|
||||
add_widget.setDate(date.today())
|
||||
case 'submission_category':
|
||||
add_widget = QComboBox()
|
||||
cats = ['Diagnostic', "Surveillance", "Research"]
|
||||
# cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
|
||||
cats += [item.name for item in SubmissionType.query()]
|
||||
try:
|
||||
cats.insert(0, cats.pop(cats.index(value)))
|
||||
except ValueError:
|
||||
cats.insert(0, cats.pop(cats.index(submission_type)))
|
||||
add_widget.addItems(cats)
|
||||
case _:
|
||||
# anything else gets added in as a line edit
|
||||
add_widget = QLineEdit()
|
||||
logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
|
||||
add_widget.setText(str(value).replace("_", " "))
|
||||
if add_widget != None:
|
||||
add_widget.setObjectName(key)
|
||||
add_widget.setParent(parent)
|
||||
|
||||
return add_widget
|
||||
|
||||
def update_missing(self):
|
||||
self.missing = True
|
||||
self.label.updated(self.objectName())
|
||||
|
||||
class ParsedQLabel(QLabel):
|
||||
|
||||
def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
|
||||
super().__init__()
|
||||
try:
|
||||
check = not value['missing']
|
||||
except:
|
||||
check = True
|
||||
if label_name != None:
|
||||
self.setObjectName(label_name)
|
||||
else:
|
||||
self.setObjectName(f"{key}_label")
|
||||
if title:
|
||||
output = key.replace('_', ' ').title()
|
||||
else:
|
||||
output = key.replace('_', ' ')
|
||||
if check:
|
||||
self.setText(f"Parsed {output}")
|
||||
else:
|
||||
self.setText(f"MISSING {output}")
|
||||
|
||||
def updated(self, key:str, title:bool=True):
|
||||
if title:
|
||||
output = key.replace('_', ' ').title()
|
||||
else:
|
||||
output = key.replace('_', ' ')
|
||||
self.setText(f"UPDATED {output}")
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
'''
|
||||
functions used by all windows in the application's frontend
|
||||
'''
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from PyQt6.QtWidgets import QMainWindow, QFileDialog
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
def select_open_file(obj:QMainWindow, file_extension:str) -> Path:
|
||||
"""
|
||||
File dialog to select a file to read from
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window to be parent
|
||||
file_extension (str): file extension
|
||||
|
||||
Returns:
|
||||
Path: Path of file to be opened
|
||||
"""
|
||||
try:
|
||||
# home_dir = Path(obj.ctx.directory_path).resolve().__str__()
|
||||
home_dir = obj.last_dir.resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().resolve().__str__()
|
||||
fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0])
|
||||
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', filter = f"{file_extension}(*.{file_extension})")[0])
|
||||
obj.last_file = fname
|
||||
return fname
|
||||
|
||||
def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:
|
||||
"""
|
||||
File dialog to select a file to write to
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window to be parent
|
||||
default_name (str): default base file name
|
||||
extension (str): file extension
|
||||
|
||||
Returns:
|
||||
Path: Path of file to be opened
|
||||
"""
|
||||
try:
|
||||
# home_dir = Path(obj.ctx.directory_path).joinpath(default_name).resolve().__str__()
|
||||
home_dir = obj.last_dir.joinpath(default_name).resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().joinpath(default_name).resolve().__str__()
|
||||
fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter = f"{extension}(*.{extension})")[0])
|
||||
# fname = Path(QFileDialog.getSaveFileName(obj, "Save File", filter = f"{extension}(*.{extension})")[0])
|
||||
obj.last_dir = fname.parent
|
||||
return fname
|
||||
|
||||
# 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
|
||||
|
||||
# Returns:
|
||||
# dict: dictionary of objectName:text items
|
||||
# """
|
||||
|
||||
# from frontend.custom_widgets import ReagentTypeForm
|
||||
# dicto = {}
|
||||
# reagents = []
|
||||
# logger.debug(f"Object type: {type(object)}")
|
||||
# # grab all widgets in form
|
||||
# try:
|
||||
# all_children = object.layout.parentWidget().findChildren(QWidget)
|
||||
# except AttributeError:
|
||||
# all_children = object.layout().parentWidget().findChildren(QWidget)
|
||||
# for item in all_children:
|
||||
# logger.debug(f"Looking at: {item.objectName()}: {type(item)}")
|
||||
# match item:
|
||||
# case QLineEdit():
|
||||
# dicto[item.objectName()] = item.text()
|
||||
# case QComboBox():
|
||||
# dicto[item.objectName()] = item.currentText()
|
||||
# case QDateEdit():
|
||||
# dicto[item.objectName()] = item.date().toPyDate()
|
||||
# case QSpinBox() | QDoubleSpinBox():
|
||||
# dicto[item.objectName()] = item.value()
|
||||
# case ReagentTypeForm():
|
||||
# reagent = extract_form_info(item)
|
||||
# logger.debug(f"Reagent found: {reagent}")
|
||||
# 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
|
||||
|
||||
|
||||
from .main_window_functions import *
|
||||
from .submission_functions import *
|
||||
@@ -1,102 +0,0 @@
|
||||
'''
|
||||
functions used by all windows in the application's frontend
|
||||
NOTE: Depreciated. Moved to functions.__init__
|
||||
'''
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from PyQt6.QtWidgets import QMainWindow, QFileDialog
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
def select_open_file(obj:QMainWindow, file_extension:str) -> Path:
|
||||
"""
|
||||
File dialog to select a file to read from
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window to be parent
|
||||
file_extension (str): file extension
|
||||
|
||||
Returns:
|
||||
Path: Path of file to be opened
|
||||
"""
|
||||
try:
|
||||
# home_dir = Path(obj.ctx.directory_path).resolve().__str__()
|
||||
home_dir = obj.last_dir.resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().resolve().__str__()
|
||||
fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0])
|
||||
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', filter = f"{file_extension}(*.{file_extension})")[0])
|
||||
obj.last_file = fname
|
||||
return fname
|
||||
|
||||
def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:
|
||||
"""
|
||||
File dialog to select a file to write to
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window to be parent
|
||||
default_name (str): default base file name
|
||||
extension (str): file extension
|
||||
|
||||
Returns:
|
||||
Path: Path of file to be opened
|
||||
"""
|
||||
try:
|
||||
# home_dir = Path(obj.ctx.directory_path).joinpath(default_name).resolve().__str__()
|
||||
home_dir = obj.last_dir.joinpath(default_name).resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().joinpath(default_name).resolve().__str__()
|
||||
fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter = f"{extension}(*.{extension})")[0])
|
||||
# fname = Path(QFileDialog.getSaveFileName(obj, "Save File", filter = f"{extension}(*.{extension})")[0])
|
||||
obj.last_dir = fname.parent
|
||||
return fname
|
||||
|
||||
# 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
|
||||
|
||||
# Returns:
|
||||
# dict: dictionary of objectName:text items
|
||||
# """
|
||||
|
||||
# from frontend.custom_widgets import ReagentTypeForm
|
||||
# dicto = {}
|
||||
# reagents = []
|
||||
# logger.debug(f"Object type: {type(object)}")
|
||||
# # grab all widgets in form
|
||||
# try:
|
||||
# all_children = object.layout.parentWidget().findChildren(QWidget)
|
||||
# except AttributeError:
|
||||
# all_children = object.layout().parentWidget().findChildren(QWidget)
|
||||
# for item in all_children:
|
||||
# logger.debug(f"Looking at: {item.objectName()}: {type(item)}")
|
||||
# match item:
|
||||
# case QLineEdit():
|
||||
# dicto[item.objectName()] = item.text()
|
||||
# case QComboBox():
|
||||
# dicto[item.objectName()] = item.currentText()
|
||||
# case QDateEdit():
|
||||
# dicto[item.objectName()] = item.date().toPyDate()
|
||||
# case QSpinBox() | QDoubleSpinBox():
|
||||
# dicto[item.objectName()] = item.value()
|
||||
# case ReagentTypeForm():
|
||||
# reagent = extract_form_info(item)
|
||||
# logger.debug(f"Reagent found: {reagent}")
|
||||
# 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
|
||||
@@ -1,935 +0,0 @@
|
||||
'''
|
||||
contains operations used by multiple widgets.
|
||||
'''
|
||||
from datetime import date
|
||||
import difflib
|
||||
import inspect
|
||||
from pprint import pformat
|
||||
import yaml
|
||||
import json
|
||||
from typing import Tuple, List
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.utils import get_column_letter
|
||||
from xhtml2pdf import pisa
|
||||
import pandas as pd
|
||||
from backend.db.models import *
|
||||
import logging
|
||||
from PyQt6.QtWidgets import QMainWindow, QPushButton
|
||||
# from .all_window_functions import select_open_file, select_save_file
|
||||
from . import select_open_file, select_save_file
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend.db.functions import (
|
||||
get_control_subtypes, update_subsampassoc_with_pcr, check_kit_integrity, update_last_used
|
||||
)
|
||||
from backend.excel.parser import SheetParser, PCRParser
|
||||
from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df
|
||||
from backend.validators import PydSubmission, PydKit
|
||||
from tools import Report, Result
|
||||
from frontend.custom_widgets.pop_ups import AlertPop, QuestionAsker
|
||||
from frontend.custom_widgets import ReportDatePicker
|
||||
from frontend.visualizations.control_charts import create_charts, construct_html
|
||||
from pathlib import Path
|
||||
from frontend.custom_widgets.misc import ReagentFormWidget
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
def import_submission_function(obj:QMainWindow, fname:Path|None=None) -> Tuple[QMainWindow, dict|None]:
|
||||
"""
|
||||
Import a new submission to the app window
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict|None]: Collection of new main app window and result dict
|
||||
"""
|
||||
logger.debug(f"\n\nStarting Import...\n\n")
|
||||
report = Report()
|
||||
# logger.debug(obj.ctx)
|
||||
# initialize samples
|
||||
try:
|
||||
obj.form.setParent(None)
|
||||
except AttributeError:
|
||||
pass
|
||||
obj.samples = []
|
||||
obj.missing_info = []
|
||||
# set file dialog
|
||||
if isinstance(fname, bool) or fname == None:
|
||||
fname = select_open_file(obj, file_extension="xlsx")
|
||||
logger.debug(f"Attempting to parse file: {fname}")
|
||||
if not fname.exists():
|
||||
# result = dict(message=f"File {fname.__str__()} not found.", status="critical")
|
||||
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
|
||||
obj.report.add_result(report)
|
||||
return obj
|
||||
# create sheetparser using excel sheet and context from gui
|
||||
try:
|
||||
obj.prsr = SheetParser(ctx=obj.ctx, filepath=fname)
|
||||
except PermissionError:
|
||||
logger.error(f"Couldn't get permission to access file: {fname}")
|
||||
return obj
|
||||
try:
|
||||
logger.debug(f"Submission dictionary:\n{pformat(obj.prsr.sub)}")
|
||||
obj.pyd = obj.prsr.to_pydantic()
|
||||
logger.debug(f"Pydantic result: \n\n{pformat(obj.pyd)}\n\n")
|
||||
except Exception as e:
|
||||
report.add_result(Result(msg=f"Problem creating pydantic model:\n\n{e}", status="critical"))
|
||||
obj.report.add_result(report)
|
||||
return obj
|
||||
obj.form = obj.pyd.toForm(parent=obj)
|
||||
obj.table_widget.formlayout.addWidget(obj.form)
|
||||
kit_widget = obj.form.find_widgets(object_name="extraction_kit")[0].input
|
||||
logger.debug(f"Kitwidget {kit_widget}")
|
||||
obj.scrape_reagents(kit_widget.currentText())
|
||||
kit_widget.currentTextChanged.connect(obj.scrape_reagents)
|
||||
# compare obj.reagents with expected reagents in kit
|
||||
if obj.prsr.sample_result != None:
|
||||
msg = AlertPop(message=obj.prsr.sample_result, status="WARNING")
|
||||
msg.exec()
|
||||
obj.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {obj.report.results}")
|
||||
logger.debug(f"All attributes of obj:\n{pformat(obj.__dict__)}")
|
||||
return obj
|
||||
|
||||
def kit_reload_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Reload the fields in the form
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
# for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
|
||||
logger.debug(f"Attempting to clear {obj.form.find_widgets()}")
|
||||
for item in obj.form.find_widgets():
|
||||
if isinstance(item, ReagentFormWidget):
|
||||
item.setParent(None)
|
||||
obj = kit_integrity_completion_function(obj)
|
||||
obj.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {obj.report.results}")
|
||||
return obj
|
||||
|
||||
def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Compare kit contents to parsed contents
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): The original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
missing_reagents = []
|
||||
logger.debug(inspect.currentframe().f_back.f_code.co_name)
|
||||
# find the widget that contains kit info
|
||||
kit_widget = obj.form.find_widgets(object_name="extraction_kit")[0].input
|
||||
logger.debug(f"Kit selector: {kit_widget}")
|
||||
# get current kit being used
|
||||
obj.ext_kit = kit_widget.currentText()
|
||||
# for reagent in obj.pyd.reagents:
|
||||
for reagent in obj.form.reagents:
|
||||
add_widget = ReagentFormWidget(parent=obj.table_widget.formwidget, reagent=reagent, extraction_kit=obj.ext_kit)
|
||||
add_widget.setParent(obj.form)
|
||||
obj.form.layout().addWidget(add_widget)
|
||||
if reagent.missing:
|
||||
missing_reagents.append(reagent)
|
||||
logger.debug(f"Checking integrity of {obj.ext_kit}")
|
||||
# TODO: put check_kit_integrity here instead of what's here?
|
||||
# see if there are any missing reagents
|
||||
if len(missing_reagents) > 0:
|
||||
result = Result(msg=f"""The submission you are importing is missing some reagents expected by the kit.\n\n
|
||||
It looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\n
|
||||
Alternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.
|
||||
\n\nPlease make sure you check the lots carefully!""".replace(" ", ""), status="Warning")
|
||||
report.add_result(result)
|
||||
if hasattr(obj.pyd, "csv"):
|
||||
export_csv_btn = QPushButton("Export CSV")
|
||||
export_csv_btn.setObjectName("export_csv_btn")
|
||||
obj.form.layout().addWidget(export_csv_btn)
|
||||
export_csv_btn.clicked.connect(obj.export_csv)
|
||||
submit_btn = QPushButton("Submit")
|
||||
submit_btn.setObjectName("submit_btn")
|
||||
obj.form.layout().addWidget(submit_btn)
|
||||
submit_btn.clicked.connect(obj.submit_new_sample)
|
||||
obj.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {obj.report.results}")
|
||||
return obj
|
||||
|
||||
def submit_new_sample_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Parse forms and add sample to the database.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
logger.debug(f"\n\nBeginning Submission\n\n")
|
||||
report = Report()
|
||||
obj.pyd: PydSubmission = obj.form.parse_form()
|
||||
logger.debug(f"Submission: {pformat(obj.pyd)}")
|
||||
logger.debug("Checking kit integrity...")
|
||||
result = check_kit_integrity(sub=obj.pyd)
|
||||
report.add_result(result)
|
||||
if len(result.results) > 0:
|
||||
obj.report.add_result(report)
|
||||
return obj
|
||||
base_submission, result = obj.pyd.toSQL()
|
||||
# check output message for issues
|
||||
match result['code']:
|
||||
# code 0: everything is fine.
|
||||
case 0:
|
||||
result = None
|
||||
# code 1: ask for overwrite
|
||||
case 1:
|
||||
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result['message'])
|
||||
if dlg.exec():
|
||||
# Do not add duplicate reagents.
|
||||
# base_submission.reagents = []
|
||||
result = None
|
||||
else:
|
||||
obj.ctx.database_session.rollback()
|
||||
return obj, dict(message="Overwrite cancelled", status="Information")
|
||||
# code 2: No RSL plate number given
|
||||
case 2:
|
||||
return obj, dict(message=result['message'], status='critical')
|
||||
case _:
|
||||
pass
|
||||
# add reagents to submission object
|
||||
for reagent in base_submission.reagents:
|
||||
update_last_used(reagent=reagent, kit=base_submission.extraction_kit)
|
||||
logger.debug(f"Here is the final submission: {pformat(base_submission.__dict__)}")
|
||||
logger.debug(f"Parsed reagents: {pformat(base_submission.reagents)}")
|
||||
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
|
||||
base_submission.save()
|
||||
# update summary sheet
|
||||
obj.table_widget.sub_wid.setData()
|
||||
# reset form
|
||||
obj.form.setParent(None)
|
||||
logger.debug(f"All attributes of obj: {pformat(obj.__dict__)}")
|
||||
wkb = obj.pyd.autofill_excel()
|
||||
if wkb != None:
|
||||
fname = select_save_file(obj=obj, default_name=obj.pyd.construct_filename(), extension="xlsx")
|
||||
try:
|
||||
wkb.save(filename=fname.__str__())
|
||||
except PermissionError:
|
||||
logger.error("Hit a permission error when saving workbook. Cancelled?")
|
||||
if hasattr(obj.pyd, 'csv'):
|
||||
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
|
||||
if dlg.exec():
|
||||
fname = select_save_file(obj, f"{obj.pyd.construct_filename()}.csv", extension="csv")
|
||||
try:
|
||||
obj.pyd.csv.to_csv(fname.__str__(), index=False)
|
||||
except PermissionError:
|
||||
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
return obj, result
|
||||
|
||||
def generate_report_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Generate a summary of activities for a time period
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
# ask for date ranges
|
||||
dlg = ReportDatePicker()
|
||||
if dlg.exec():
|
||||
info = dlg.parse_form()
|
||||
logger.debug(f"Report info: {info}")
|
||||
# find submissions based on date range
|
||||
subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date'])
|
||||
# convert each object to dict
|
||||
records = [item.report_dict() for item in subs]
|
||||
# make dataframe from record dictionaries
|
||||
detailed_df, summary_df = make_report_xlsx(records=records)
|
||||
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
||||
# get save location of report
|
||||
fname = select_save_file(obj=obj, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf", extension="pdf")
|
||||
with open(fname, "w+b") as f:
|
||||
pisa.CreatePDF(html, dest=f)
|
||||
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
||||
summary_df.to_excel(writer, sheet_name="Report")
|
||||
detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
||||
worksheet = writer.sheets['Report']
|
||||
for idx, col in enumerate(summary_df): # loop through all columns
|
||||
series = summary_df[col]
|
||||
max_len = max((
|
||||
series.astype(str).map(len).max(), # len of largest item
|
||||
len(str(series.name)) # len of column name/header
|
||||
)) + 20 # adding a little extra space
|
||||
try:
|
||||
worksheet.column_dimensions[get_column_letter(idx)].width = max_len
|
||||
except ValueError:
|
||||
pass
|
||||
for cell in worksheet['D']:
|
||||
if cell.row > 1:
|
||||
cell.style = 'Currency'
|
||||
writer.close()
|
||||
return obj, None
|
||||
|
||||
def add_kit_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Add a new kit to the database.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
# setup file dialog to find yaml file
|
||||
fname = select_open_file(obj, file_extension="yml")
|
||||
assert fname.exists()
|
||||
# read yaml file
|
||||
try:
|
||||
with open(fname.__str__(), "r") as stream:
|
||||
try:
|
||||
exp = yaml.load(stream, Loader=yaml.Loader)
|
||||
except yaml.YAMLError as exc:
|
||||
logger.error(f'Error reading yaml file {fname}: {exc}')
|
||||
return {}
|
||||
except PermissionError:
|
||||
return
|
||||
# send to kit creator function
|
||||
result = PydKit(**exp)
|
||||
return obj, result
|
||||
|
||||
def add_org_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Add a new organization to the database.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
# setup file dialog to find yaml flie
|
||||
fname = select_open_file(obj, extension="yml")
|
||||
assert fname.exists()
|
||||
# read yaml file
|
||||
try:
|
||||
with open(fname.__str__(), "r") as stream:
|
||||
try:
|
||||
org = yaml.load(stream, Loader=yaml.Loader)
|
||||
except yaml.YAMLError as exc:
|
||||
logger.error(f'Error reading yaml file {fname}: {exc}')
|
||||
return obj, dict(message=f"There was a problem reading yaml file {fname.__str__()}", status="critical")
|
||||
except PermissionError:
|
||||
return obj, result
|
||||
# send to kit creator function
|
||||
result = construct_org_from_yaml(ctx=obj.ctx, org=org)
|
||||
return obj, result
|
||||
|
||||
def controls_getter_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Get controls based on start/end dates
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
# subtype defaults to disabled
|
||||
try:
|
||||
obj.table_widget.sub_typer.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
# correct start date being more recent than end date and rerun
|
||||
if obj.table_widget.datepicker.start_date.date() > obj.table_widget.datepicker.end_date.date():
|
||||
logger.warning("Start date after end date is not allowed!")
|
||||
threemonthsago = obj.table_widget.datepicker.end_date.date().addDays(-60)
|
||||
# block signal that will rerun controls getter and set start date
|
||||
# Without triggering this function again
|
||||
with QSignalBlocker(obj.table_widget.datepicker.start_date) as blocker:
|
||||
obj.table_widget.datepicker.start_date.setDate(threemonthsago)
|
||||
obj._controls_getter()
|
||||
obj.report.add_result(report)
|
||||
return obj
|
||||
# convert to python useable date objects
|
||||
obj.start_date = obj.table_widget.datepicker.start_date.date().toPyDate()
|
||||
obj.end_date = obj.table_widget.datepicker.end_date.date().toPyDate()
|
||||
obj.con_type = obj.table_widget.control_typer.currentText()
|
||||
obj.mode = obj.table_widget.mode_typer.currentText()
|
||||
obj.table_widget.sub_typer.clear()
|
||||
# lookup subtypes
|
||||
sub_types = get_control_subtypes(type=obj.con_type, mode=obj.mode)
|
||||
# sub_types = lookup_controls(ctx=obj.ctx, control_type=obj.con_type)
|
||||
if sub_types != []:
|
||||
# block signal that will rerun controls getter and update sub_typer
|
||||
with QSignalBlocker(obj.table_widget.sub_typer) as blocker:
|
||||
obj.table_widget.sub_typer.addItems(sub_types)
|
||||
obj.table_widget.sub_typer.setEnabled(True)
|
||||
obj.table_widget.sub_typer.currentTextChanged.connect(obj._chart_maker)
|
||||
else:
|
||||
obj.table_widget.sub_typer.clear()
|
||||
obj.table_widget.sub_typer.setEnabled(False)
|
||||
obj._chart_maker()
|
||||
obj.report.add_result(report)
|
||||
return obj
|
||||
|
||||
def chart_maker_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Create html chart for controls reporting
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
logger.debug(f"Control getter context: \n\tControl type: {obj.con_type}\n\tMode: {obj.mode}\n\tStart Date: {obj.start_date}\n\tEnd Date: {obj.end_date}")
|
||||
# set the subtype for kraken
|
||||
if obj.table_widget.sub_typer.currentText() == "":
|
||||
obj.subtype = None
|
||||
else:
|
||||
obj.subtype = obj.table_widget.sub_typer.currentText()
|
||||
logger.debug(f"Subtype: {obj.subtype}")
|
||||
# query all controls using the type/start and end dates from the gui
|
||||
# controls = get_all_controls_by_type(ctx=obj.ctx, con_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
|
||||
# controls = lookup_controls(ctx=obj.ctx, control_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
|
||||
controls = Control.query(control_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
|
||||
# if no data found from query set fig to none for reporting in webview
|
||||
if controls == None:
|
||||
fig = None
|
||||
else:
|
||||
# change each control to list of dictionaries
|
||||
data = [control.convert_by_mode(mode=obj.mode) for control in controls]
|
||||
# flatten data to one dimensional list
|
||||
data = [item for sublist in data for item in sublist]
|
||||
logger.debug(f"Control objects going into df conversion: {type(data)}")
|
||||
if data == []:
|
||||
return obj, dict(status="Critical", message="No data found for controls in given date range.")
|
||||
# send to dataframe creator
|
||||
df = convert_data_list_to_df(input=data, subtype=obj.subtype)
|
||||
if obj.subtype == None:
|
||||
title = obj.mode
|
||||
else:
|
||||
title = f"{obj.mode} - {obj.subtype}"
|
||||
# send dataframe to chart maker
|
||||
fig = create_charts(ctx=obj.ctx, df=df, ytitle=title)
|
||||
logger.debug(f"Updating figure...")
|
||||
# construct html for webview
|
||||
html = construct_html(figure=fig)
|
||||
logger.debug(f"The length of html code is: {len(html)}")
|
||||
obj.table_widget.webengineview.setHtml(html)
|
||||
obj.table_widget.webengineview.update()
|
||||
logger.debug("Figure updated... I hope.")
|
||||
obj.report.add_result(report)
|
||||
return obj
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
# all_bcs = lookup_all_submissions_by_type(obj.ctx, "Bacterial Culture")
|
||||
# all_bcs = lookup_submissions(ctx=obj.ctx, submission_type="Bacterial Culture")
|
||||
all_bcs = BasicSubmission.query(submission_type="Bacterial Culture")
|
||||
logger.debug(all_bcs)
|
||||
# all_controls = get_all_controls(obj.ctx)
|
||||
# all_controls = lookup_controls(ctx=obj.ctx)
|
||||
all_controls = Control.query()
|
||||
ac_list = [control.name for control in all_controls]
|
||||
count = 0
|
||||
for bcs in all_bcs:
|
||||
logger.debug(f"Running for {bcs.rsl_plate_num}")
|
||||
logger.debug(f"Here is the current control: {[control.name for control in bcs.controls]}")
|
||||
samples = [sample.submitter_id for sample in bcs.samples]
|
||||
logger.debug(bcs.controls)
|
||||
for sample in samples:
|
||||
# replace below is a stopgap method because some dingus decided to add spaces in some of the ATCC49... so it looks like "ATCC 49"...
|
||||
if " " in sample:
|
||||
logger.warning(f"There is not supposed to be a space in the sample name!!!")
|
||||
sample = sample.replace(" ", "")
|
||||
if not any([ac.startswith(sample) for ac in ac_list]):
|
||||
continue
|
||||
else:
|
||||
for control in all_controls:
|
||||
diff = difflib.SequenceMatcher(a=sample, b=control.name).ratio()
|
||||
if control.name.startswith(sample):
|
||||
logger.debug(f"Checking {sample} against {control.name}... {diff}")
|
||||
logger.debug(f"Found match:\n\tSample: {sample}\n\tControl: {control.name}\n\tDifference: {diff}")
|
||||
if control in bcs.controls:
|
||||
logger.debug(f"{control.name} already in {bcs.rsl_plate_num}, skipping")
|
||||
continue
|
||||
else:
|
||||
logger.debug(f"Adding {control.name} to {bcs.rsl_plate_num} as control")
|
||||
bcs.controls.append(control)
|
||||
control.submission = bcs
|
||||
control.submission_id = bcs.id
|
||||
obj.ctx.database_session.add(control)
|
||||
count += 1
|
||||
obj.ctx.database_session.add(bcs)
|
||||
logger.debug(f"Here is the new control: {[control.name for control in bcs.controls]}")
|
||||
result = dict(message=f"We added {count} controls to bacterial cultures.", status="information")
|
||||
logger.debug(result)
|
||||
obj.ctx.database_session.commit()
|
||||
return obj, result
|
||||
|
||||
def link_extractions_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Link extractions from runlogs to imported submissions
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
fname = select_open_file(obj, file_extension="csv")
|
||||
with open(fname.__str__(), 'r') as f:
|
||||
# split csv on commas
|
||||
runs = [col.strip().split(",") for col in f.readlines()]
|
||||
count = 0
|
||||
for run in runs:
|
||||
new_run = dict(
|
||||
start_time=run[0].strip(),
|
||||
rsl_plate_num=run[1].strip(),
|
||||
sample_count=run[2].strip(),
|
||||
status=run[3].strip(),
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# elution columns are item 6 in the comma split list to the end
|
||||
for ii in range(6, len(run)):
|
||||
new_run[f"column{str(ii-5)}_vol"] = run[ii]
|
||||
# Lookup imported submissions
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
||||
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
||||
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
|
||||
# If no such submission exists, move onto the next run
|
||||
if sub == None:
|
||||
continue
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
count += 1
|
||||
except AttributeError:
|
||||
continue
|
||||
if sub.extraction_info != None:
|
||||
existing = json.loads(sub.extraction_info)
|
||||
else:
|
||||
existing = None
|
||||
# Check if the new info already exists in the imported submission
|
||||
try:
|
||||
if json.dumps(new_run) in sub.extraction_info:
|
||||
logger.debug(f"Looks like we already have that info.")
|
||||
continue
|
||||
except TypeError:
|
||||
pass
|
||||
# Update or create the extraction info
|
||||
if existing != None:
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}")
|
||||
existing.append(new_run)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.extraction_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.extraction_info = json.dumps([new_run])
|
||||
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.extraction_info}")
|
||||
else:
|
||||
sub.extraction_info = json.dumps([new_run])
|
||||
obj.ctx.database_session.add(sub)
|
||||
obj.ctx.database_session.commit()
|
||||
result = dict(message=f"We added {count} logs to the database.", status='information')
|
||||
return obj, result
|
||||
|
||||
def link_pcr_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Link PCR data from run logs to an imported submission
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
fname = select_open_file(obj, file_extension="csv")
|
||||
with open(fname.__str__(), 'r') as f:
|
||||
# split csv rows on comma
|
||||
runs = [col.strip().split(",") for col in f.readlines()]
|
||||
count = 0
|
||||
for run in runs:
|
||||
new_run = dict(
|
||||
start_time=run[0].strip(),
|
||||
rsl_plate_num=run[1].strip(),
|
||||
biomek_status=run[2].strip(),
|
||||
quant_status=run[3].strip(),
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# lookup imported submission
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
||||
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
||||
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
|
||||
# if imported submission doesn't exist move on to next run
|
||||
if sub == None:
|
||||
continue
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
continue
|
||||
# check if pcr_info already exists
|
||||
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
|
||||
existing = json.loads(sub.pcr_info)
|
||||
else:
|
||||
existing = None
|
||||
# check if this entry already exists in imported submission
|
||||
try:
|
||||
if json.dumps(new_run) in sub.pcr_info:
|
||||
logger.debug(f"Looks like we already have that info.")
|
||||
continue
|
||||
else:
|
||||
count += 1
|
||||
except TypeError:
|
||||
logger.error(f"No json to dump")
|
||||
if existing != None:
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}")
|
||||
existing.append(new_run)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.pcr_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.pcr_info = json.dumps([new_run])
|
||||
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.pcr_info}")
|
||||
else:
|
||||
sub.pcr_info = json.dumps([new_run])
|
||||
obj.ctx.database_session.add(sub)
|
||||
obj.ctx.database_session.commit()
|
||||
result = dict(message=f"We added {count} logs to the database.", status='information')
|
||||
return obj, result
|
||||
|
||||
def import_pcr_results_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Import Quant-studio PCR data to an imported submission
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
result = None
|
||||
fname = select_open_file(obj, file_extension="xlsx")
|
||||
parser = PCRParser(filepath=fname)
|
||||
logger.debug(f"Attempting lookup for {parser.plate_num}")
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
|
||||
sub = BasicSubmission.query(rsl_number=parser.plate_num)
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
# If no plate is found, may be because this is a repeat. Lop off the '-1' or '-2' and repeat
|
||||
logger.error(f"Submission of number {parser.plate_num} not found. Attempting rescue of plate repeat.")
|
||||
parser.plate_num = "-".join(parser.plate_num.split("-")[:-1])
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
|
||||
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=parser.plate_num)
|
||||
sub = BasicSubmission.query(rsl_number=parser.plate_num)
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
logger.error(f"Rescue of {parser.plate_num} failed.")
|
||||
return obj, dict(message="Couldn't find a submission with that RSL number.", status="warning")
|
||||
# Check if PCR info already exists
|
||||
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
|
||||
existing = json.loads(sub.pcr_info)
|
||||
else:
|
||||
existing = None
|
||||
if existing != None:
|
||||
# update pcr_info
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(parser.pcr)}: {parser.pcr}")
|
||||
if json.dumps(parser.pcr) not in sub.pcr_info:
|
||||
existing.append(parser.pcr)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.pcr_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.pcr_info = json.dumps([parser.pcr])
|
||||
logger.debug(f"Final pcr info for {sub.rsl_plate_num}: {sub.pcr_info}")
|
||||
else:
|
||||
sub.pcr_info = json.dumps([parser.pcr])
|
||||
obj.ctx.database_session.add(sub)
|
||||
logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}")
|
||||
logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}")
|
||||
obj.ctx.database_session.commit()
|
||||
logger.debug(f"Got {len(parser.samples)} samples to update!")
|
||||
logger.debug(f"Parser samples: {parser.samples}")
|
||||
for sample in sub.samples:
|
||||
logger.debug(f"Running update on: {sample}")
|
||||
try:
|
||||
sample_dict = [item for item in parser.samples if item['sample']==sample.rsl_number][0]
|
||||
except IndexError:
|
||||
continue
|
||||
update_subsampassoc_with_pcr(submission=sub, sample=sample, input_dict=sample_dict)
|
||||
|
||||
result = dict(message=f"We added PCR info to {sub.rsl_plate_num}.", status='information')
|
||||
return obj, result
|
||||
|
||||
def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_reagents:List[str], info:dict, missing_info:List[str]):
|
||||
"""
|
||||
Automatically fills in excel cells with submission info.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window
|
||||
xl_map (dict): Map of where each item goes in the excel workbook.
|
||||
reagents (List[dict]): All reagents placed in the submission form.
|
||||
missing_reagents (List[str]): Reagents that are required for the kit that were not present.
|
||||
info (dict): Dictionary of misc info from submission
|
||||
missing_info (List[str]): Plate info missing from the excel sheet.
|
||||
"""
|
||||
# logger.debug(reagents)
|
||||
|
||||
logger.debug(f"Here is the info dict coming in:\n{pprint.pformat(info)}")
|
||||
logger.debug(f"Here are the missing reagents:\n{missing_reagents}")
|
||||
logger.debug(f"Here are the missing info:\n{missing_info}")
|
||||
logger.debug(f"Here is the xl map: {pprint.pformat(xl_map)}")
|
||||
# pare down the xl map to only the missing data.
|
||||
relevant_reagent_map = {k:v for k,v in xl_map.items() if k in [reagent.type for reagent in missing_reagents]}
|
||||
# pare down reagents to only what's missing
|
||||
logger.debug(f"Checking {[item['type'] for item in reagents]} against {[reagent.type for reagent in missing_reagents]}")
|
||||
relevant_reagents = [item for item in reagents if item['type'] in [reagent.type for reagent in missing_reagents]]
|
||||
logger.debug(f"Here are the relevant reagents: {pprint.pformat(relevant_reagents)}")
|
||||
# hacky manipulation of submission type so it looks better.
|
||||
# pare down info to just what's missing
|
||||
relevant_info_map = {k:v for k,v in xl_map['info'].items() if k in missing_info and k != 'samples'}
|
||||
relevant_info = {k:v for k,v in info.items() if k in missing_info}
|
||||
logger.debug(f"Here is the relevant info: {pprint.pformat(relevant_info)}")
|
||||
# construct new objects to put into excel sheets:
|
||||
new_reagents = []
|
||||
logger.debug(f"Parsing from relevant reagent map: {pprint.pformat(relevant_reagent_map)}")
|
||||
for reagent in relevant_reagents:
|
||||
new_reagent = {}
|
||||
new_reagent['type'] = reagent['type']
|
||||
new_reagent['lot'] = relevant_reagent_map[new_reagent['type']]['lot']
|
||||
new_reagent['lot']['value'] = reagent['lot']
|
||||
new_reagent['expiry'] = relevant_reagent_map[new_reagent['type']]['expiry']
|
||||
new_reagent['expiry']['value'] = reagent['expiry']
|
||||
new_reagent['sheet'] = relevant_reagent_map[new_reagent['type']]['sheet']
|
||||
# name is only present for Bacterial Culture
|
||||
try:
|
||||
new_reagent['name'] = relevant_reagent_map[new_reagent['type']]['name']
|
||||
new_reagent['name']['value'] = reagent['name']
|
||||
except Exception as e:
|
||||
logger.error(f"Couldn't get name due to {e}")
|
||||
new_reagents.append(new_reagent)
|
||||
# construct new info objects to put into excel sheets
|
||||
new_info = []
|
||||
logger.debug(f"Parsing from relevant info map: {pprint.pformat(relevant_info_map)}")
|
||||
for item in relevant_info:
|
||||
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
|
||||
workbook = load_workbook(obj.prsr.xl.io)
|
||||
# get list of sheet names
|
||||
sheets = workbook.sheetnames
|
||||
# logger.debug(workbook.sheetnames)
|
||||
for sheet in sheets:
|
||||
# open sheet
|
||||
worksheet=workbook[sheet]
|
||||
# Get relevant reagents for that sheet
|
||||
sheet_reagents = [item for item in new_reagents if sheet in item['sheet']]
|
||||
for reagent in sheet_reagents:
|
||||
logger.debug(f"Attempting to write lot {reagent['lot']['value']} in: row {reagent['lot']['row']}, column {reagent['lot']['column']}")
|
||||
worksheet.cell(row=reagent['lot']['row'], column=reagent['lot']['column'], value=reagent['lot']['value'])
|
||||
logger.debug(f"Attempting to write expiry {reagent['expiry']['value']} in: row {reagent['expiry']['row']}, column {reagent['expiry']['column']}")
|
||||
worksheet.cell(row=reagent['expiry']['row'], column=reagent['expiry']['column'], value=reagent['expiry']['value'])
|
||||
try:
|
||||
logger.debug(f"Attempting to write name {reagent['name']['value']} in: row {reagent['name']['row']}, column {reagent['name']['column']}")
|
||||
worksheet.cell(row=reagent['name']['row'], column=reagent['name']['column'], value=reagent['name']['value'])
|
||||
except Exception as e:
|
||||
logger.error(f"Could not write name {reagent['name']['value']} due to {e}")
|
||||
# Get relevant info for that sheet
|
||||
sheet_info = [item for item in new_info if sheet in item['location']['sheets']]
|
||||
for item in sheet_info:
|
||||
logger.debug(f"Attempting: {item['type']}")
|
||||
worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value'])
|
||||
# Hacky way to pop in 'signed by'
|
||||
# custom_parser = get_polymorphic_subclass(BasicSubmission, info['submission_type'])
|
||||
custom_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=info['submission_type'])
|
||||
workbook = custom_parser.custom_autofill(workbook)
|
||||
fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx")
|
||||
workbook.save(filename=fname.__str__())
|
||||
|
||||
# def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
||||
# """
|
||||
# Generates a csv file from client submitted xlsx file.
|
||||
# NOTE: Depreciated, now folded into import Artic.
|
||||
|
||||
# Args:
|
||||
# obj (QMainWindow): Main application
|
||||
|
||||
# Returns:
|
||||
# Tuple[QMainWindow, dict]: Updated main application and result
|
||||
# """
|
||||
# def get_plates(input_sample_number:str, plates:list) -> Tuple[int, str]:
|
||||
# logger.debug(f"Looking up {input_sample_number} in {plates}")
|
||||
# # samp = lookup_samples(ctx=obj.ctx, ww_processing_num=input_sample_number)
|
||||
# samp = BasicSample.query(ww_processing_num=input_sample_number)
|
||||
# if samp == None:
|
||||
# # samp = lookup_samples(ctx=obj.ctx, submitter_id=input_sample_number)
|
||||
# samp = BasicSample.query(submitter_id=input_sample_number)
|
||||
# if samp == None:
|
||||
# return None, None
|
||||
# logger.debug(f"Got sample: {samp}")
|
||||
# # new_plates = [(iii+1, lookup_submission_sample_association(ctx=obj.ctx, sample=samp, submission=plate)) for iii, plate in enumerate(plates)]
|
||||
# new_plates = [(iii+1, SubmissionSampleAssociation.query(sample=samp, submission=plate)) for iii, plate in enumerate(plates)]
|
||||
# logger.debug(f"Associations: {pformat(new_plates)}")
|
||||
# try:
|
||||
# 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}")
|
||||
# return plate_num, plate
|
||||
# fname = select_open_file(obj=obj, file_extension="xlsx")
|
||||
# xl = pd.ExcelFile(fname)
|
||||
# sprsr = SampleParser(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()
|
||||
# # Fix no plates found in form.
|
||||
# if plates == []:
|
||||
# dlg = FirstStrandPlateList(ctx=obj.ctx)
|
||||
# if dlg.exec():
|
||||
# plates = dlg.parse_form()
|
||||
# plates = list(set(plates))
|
||||
# logger.debug(f"Plates: {pformat(plates)}")
|
||||
# output_samples = []
|
||||
# logger.debug(f"Samples: {pformat(samples)}")
|
||||
# old_plate_number = 1
|
||||
# old_plate = ''
|
||||
# for item in samples:
|
||||
# try:
|
||||
# item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0]
|
||||
# except AttributeError:
|
||||
# pass
|
||||
# item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip()
|
||||
# new_dict = {}
|
||||
# new_dict['sample'] = item['submitter_id']
|
||||
# plate_num, plate = get_plates(input_sample_number=new_dict['sample'], plates=plates)
|
||||
# if plate_num == None:
|
||||
# plate_num = str(old_plate_number) + "*"
|
||||
# else:
|
||||
# old_plate_number = plate_num
|
||||
# logger.debug(f"Got plate number: {plate_num}, plate: {plate}")
|
||||
# if item['submitter_id'] == "NTC1":
|
||||
# new_dict['destination_row'] = 8
|
||||
# new_dict['destination_column'] = 2
|
||||
# new_dict['plate_number'] = 'control'
|
||||
# new_dict['plate'] = None
|
||||
# output_samples.append(new_dict)
|
||||
# continue
|
||||
# elif item['submitter_id'] == "NTC2":
|
||||
# new_dict['destination_row'] = 8
|
||||
# new_dict['destination_column'] = 5
|
||||
# new_dict['plate_number'] = 'control'
|
||||
# new_dict['plate'] = None
|
||||
# output_samples.append(new_dict)
|
||||
# continue
|
||||
# else:
|
||||
# new_dict['destination_row'] = item['row']
|
||||
# new_dict['destination_column'] = item['column']
|
||||
# new_dict['plate_number'] = plate_num
|
||||
# # Fix plate association not found
|
||||
# if plate == None:
|
||||
# dlg = FirstStrandSalvage(ctx=obj.ctx, submitter_id=item['submitter_id'], rsl_plate_num=old_plate)
|
||||
# if dlg.exec():
|
||||
# item.update(dlg.parse_form())
|
||||
# try:
|
||||
# new_dict['source_row'], new_dict['source_column'] = convert_well_to_row_column(item['well'])
|
||||
# except KeyError:
|
||||
# pass
|
||||
# else:
|
||||
# new_dict['plate'] = plate.submission.rsl_plate_num
|
||||
# new_dict['source_row'] = plate.row
|
||||
# new_dict['source_column'] = plate.column
|
||||
# old_plate = plate.submission.rsl_plate_num
|
||||
# output_samples.append(new_dict)
|
||||
# df = pd.DataFrame.from_records(output_samples)
|
||||
# df.sort_values(by=['destination_column', 'destination_row'], ascending=True, inplace=True)
|
||||
# columnsTitles = ['sample', 'destination_column', 'destination_row', 'plate_number', 'plate', "source_column", 'source_row']
|
||||
# df = df.reindex(columns=columnsTitles)
|
||||
# ofname = select_save_file(obj=obj, default_name=f"First Strand {date.today()}", extension="csv")
|
||||
# df.to_csv(ofname, index=False)
|
||||
# return obj, None
|
||||
|
||||
def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, dict]:
|
||||
"""
|
||||
Extracted scrape reagents function that will run when
|
||||
form 'extraction_kit' widget is updated.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): updated main application
|
||||
extraction_kit (str): name of extraction kit (in 'extraction_kit' widget)
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Updated application and result
|
||||
"""
|
||||
report = Report()
|
||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||
# obj.reagents = []
|
||||
# obj.missing_reagents = []
|
||||
# Remove previous reagent widgets
|
||||
try:
|
||||
old_reagents = obj.form.find_widgets()
|
||||
except AttributeError:
|
||||
logger.error(f"Couldn't find old reagents.")
|
||||
old_reagents = []
|
||||
# logger.debug(f"\n\nAttempting to clear: {old_reagents}\n\n")
|
||||
for reagent in old_reagents:
|
||||
if isinstance(reagent, ReagentFormWidget) or isinstance(reagent, QPushButton):
|
||||
reagent.setParent(None)
|
||||
# reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
|
||||
# logger.debug(f"Got reagents: {reagents}")
|
||||
# for reagent in obj.prsr.sub['reagents']:
|
||||
# # create label
|
||||
# if reagent.parsed:
|
||||
# obj.reagents.append(reagent)
|
||||
# else:
|
||||
# obj.missing_reagents.append(reagent)
|
||||
obj.form.reagents = obj.prsr.sub['reagents']
|
||||
# logger.debug(f"Imported reagents: {obj.reagents}")
|
||||
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
||||
obj.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {obj.report.results}")
|
||||
return obj
|
||||
|
||||
def export_csv_function(obj:QMainWindow, fname:Path|None=None):
|
||||
if isinstance(fname, bool) or fname == None:
|
||||
fname = select_save_file(obj=obj, default_name=obj.pyd.construct_filename(), extension="csv")
|
||||
try:
|
||||
obj.pyd.csv.to_csv(fname.__str__(), index=False)
|
||||
except PermissionError:
|
||||
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
@@ -51,7 +51,7 @@ def create_charts(ctx:dict, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
|
||||
df = df.sort_values(by=sorts, ascending=ascending)
|
||||
logger.debug(df[df.isna().any(axis=1)])
|
||||
# actual chart construction is done by
|
||||
fig = construct_chart(ctx=ctx, df=df, modes=modes, ytitle=ytitle)
|
||||
fig = construct_chart(df=df, modes=modes, ytitle=ytitle)
|
||||
return fig
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ def output_figures(settings:dict, figs:list, group_name:str):
|
||||
|
||||
|
||||
|
||||
def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None) -> Figure:
|
||||
def construct_chart(df:pd.DataFrame, modes:list, ytitle:str|None=None) -> Figure:
|
||||
"""
|
||||
Creates a plotly chart for controls from a pandas dataframe
|
||||
|
||||
|
||||
13
src/submissions/frontend/widgets/__init__.py
Normal file
13
src/submissions/frontend/widgets/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
'''
|
||||
Contains all custom generated PyQT6 derivative widgets.
|
||||
'''
|
||||
|
||||
# from .app import App
|
||||
from .functions import *
|
||||
from .misc import *
|
||||
from .pop_ups import *
|
||||
from .submission_table import *
|
||||
from .submission_widget import *
|
||||
from .controls_chart import *
|
||||
from .kit_creator import *
|
||||
from .app import App
|
||||
412
src/submissions/frontend/widgets/app.py
Normal file
412
src/submissions/frontend/widgets/app.py
Normal file
@@ -0,0 +1,412 @@
|
||||
'''
|
||||
Constructs main application.
|
||||
TODO: Complete.
|
||||
'''
|
||||
import sys
|
||||
from PyQt6.QtWidgets import (
|
||||
QTabWidget, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QScrollArea, QMainWindow,
|
||||
QToolBar
|
||||
)
|
||||
from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from backend.validators import PydReagent
|
||||
# from frontend.functions import (
|
||||
# add_kit_function, add_org_function, link_controls_function, export_csv_function
|
||||
# )
|
||||
from tools import check_if_app, Settings, Report
|
||||
from .pop_ups import AlertPop
|
||||
from .misc import AddReagentForm
|
||||
import logging
|
||||
from datetime import date
|
||||
import webbrowser
|
||||
from .submission_table import SubmissionsSheet
|
||||
from .submission_widget import SubmissionFormContainer
|
||||
from .controls_chart import ControlsViewer
|
||||
from .kit_creator import KitAdder
|
||||
import webbrowser
|
||||
|
||||
logger = logging.getLogger(f'submissions.{__name__}')
|
||||
logger.info("Hello, I am a logger")
|
||||
|
||||
class App(QMainWindow):
|
||||
|
||||
def __init__(self, ctx: Settings = None):
|
||||
logger.debug(f"Initializing main window...")
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
self.last_dir = ctx.directory_path
|
||||
self.report = Report()
|
||||
# indicate version and connected database in title bar
|
||||
try:
|
||||
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}"
|
||||
except (AttributeError, KeyError):
|
||||
self.title = f"Submissions App"
|
||||
# set initial app position and size
|
||||
self.left = 0
|
||||
self.top = 0
|
||||
self.width = 1300
|
||||
self.height = 1000
|
||||
self.setWindowTitle(self.title)
|
||||
self.setGeometry(self.left, self.top, self.width, self.height)
|
||||
# insert tabs into main app
|
||||
self.table_widget = AddSubForm(self)
|
||||
self.setCentralWidget(self.table_widget)
|
||||
# run initial setups
|
||||
self._createActions()
|
||||
self._createMenuBar()
|
||||
self._createToolBar()
|
||||
self._connectActions()
|
||||
# self._controls_getter()
|
||||
self.show()
|
||||
self.statusBar().showMessage('Ready', 5000)
|
||||
|
||||
def _createMenuBar(self):
|
||||
"""
|
||||
adds items to menu bar
|
||||
"""
|
||||
logger.debug(f"Creating menu bar...")
|
||||
menuBar = self.menuBar()
|
||||
fileMenu = menuBar.addMenu("&File")
|
||||
# Creating menus using a title
|
||||
# methodsMenu = menuBar.addMenu("&Methods")
|
||||
reportMenu = menuBar.addMenu("&Reports")
|
||||
maintenanceMenu = menuBar.addMenu("&Monthly")
|
||||
helpMenu = menuBar.addMenu("&Help")
|
||||
helpMenu.addAction(self.helpAction)
|
||||
helpMenu.addAction(self.docsAction)
|
||||
fileMenu.addAction(self.importAction)
|
||||
fileMenu.addAction(self.importPCRAction)
|
||||
# methodsMenu.addAction(self.constructFS)
|
||||
reportMenu.addAction(self.generateReportAction)
|
||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||
maintenanceMenu.addAction(self.joinPCRAction)
|
||||
|
||||
def _createToolBar(self):
|
||||
"""
|
||||
adds items to toolbar
|
||||
"""
|
||||
logger.debug(f"Creating toolbar...")
|
||||
toolbar = QToolBar("My main toolbar")
|
||||
self.addToolBar(toolbar)
|
||||
toolbar.addAction(self.addReagentAction)
|
||||
toolbar.addAction(self.addKitAction)
|
||||
toolbar.addAction(self.addOrgAction)
|
||||
|
||||
def _createActions(self):
|
||||
"""
|
||||
creates actions
|
||||
"""
|
||||
logger.debug(f"Creating actions...")
|
||||
self.importAction = QAction("&Import Submission", self)
|
||||
self.importPCRAction = QAction("&Import PCR Results", self)
|
||||
self.addReagentAction = QAction("Add Reagent", self)
|
||||
self.generateReportAction = QAction("Make Report", self)
|
||||
self.addKitAction = QAction("Import Kit", self)
|
||||
self.addOrgAction = QAction("Import Org", self)
|
||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||
self.joinPCRAction = QAction("Link PCR Logs")
|
||||
self.helpAction = QAction("&About", self)
|
||||
self.docsAction = QAction("&Docs", self)
|
||||
# self.constructFS = QAction("Make First Strand", self)
|
||||
|
||||
def _connectActions(self):
|
||||
"""
|
||||
connect menu and tool bar item to functions
|
||||
"""
|
||||
logger.debug(f"Connecting actions...")
|
||||
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
|
||||
self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results)
|
||||
self.addReagentAction.triggered.connect(self.add_reagent)
|
||||
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
|
||||
# self.addKitAction.triggered.connect(self.add_kit)
|
||||
# self.addOrgAction.triggered.connect(self.add_org)
|
||||
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
|
||||
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
|
||||
self.helpAction.triggered.connect(self.showAbout)
|
||||
self.docsAction.triggered.connect(self.openDocs)
|
||||
# self.constructFS.triggered.connect(self.construct_first_strand)
|
||||
# self.table_widget.formwidget.import_drag.connect(self.importSubmission)
|
||||
|
||||
def showAbout(self):
|
||||
"""
|
||||
Show the 'about' message
|
||||
"""
|
||||
output = f"Version: {self.ctx.package.__version__}\n\nAuthor: {self.ctx.package.__author__['name']} - {self.ctx.package.__author__['email']}\n\nCopyright: {self.ctx.package.__copyright__}"
|
||||
about = AlertPop(message=output, status="information")
|
||||
about.exec()
|
||||
|
||||
def openDocs(self):
|
||||
"""
|
||||
Open the documentation html pages
|
||||
"""
|
||||
if check_if_app():
|
||||
url = Path(sys._MEIPASS).joinpath("files", "docs", "index.html")
|
||||
else:
|
||||
url = Path("docs\\build\\index.html").absolute()
|
||||
logger.debug(f"Attempting to open {url}")
|
||||
webbrowser.get('windows-default').open(f"file://{url.__str__()}")
|
||||
|
||||
def result_reporter(self):
|
||||
# def result_reporter(self, result:TypedDict[]|None=None):
|
||||
"""
|
||||
Report any anomolous results - if any - to the user
|
||||
|
||||
Args:
|
||||
result (dict | None, optional): The result from a function. Defaults to None.
|
||||
"""
|
||||
# logger.info(f"We got the result: {result}")
|
||||
# if result != None:
|
||||
# msg = AlertPop(message=result['message'], status=result['status'])
|
||||
# msg.exec()
|
||||
logger.debug(f"Running results reporter for: {self.report.results}")
|
||||
if len(self.report.results) > 0:
|
||||
logger.debug(f"We've got some results!")
|
||||
for result in self.report.results:
|
||||
logger.debug(f"Showing result: {result}")
|
||||
if result != None:
|
||||
alert = result.report()
|
||||
if alert.exec():
|
||||
pass
|
||||
self.report = Report()
|
||||
else:
|
||||
self.statusBar().showMessage("Action completed sucessfully.", 5000)
|
||||
|
||||
# def importSubmission(self, fname:Path|None=None):
|
||||
# """
|
||||
# import submission from excel sheet into form
|
||||
# """
|
||||
# # from .main_window_functions import import_submission_function
|
||||
# self.raise_()
|
||||
# self.activateWindow()
|
||||
# self = import_submission_function(self, fname)
|
||||
# logger.debug(f"Result from result reporter: {self.report.results}")
|
||||
# self.result_reporter()
|
||||
|
||||
# def kit_reload(self):
|
||||
# """
|
||||
# Removes all reagents from form before running kit integrity completion.
|
||||
# """
|
||||
# # from .main_window_functions import kit_reload_function
|
||||
# self = kit_reload_function(self)
|
||||
# self.result_reporter()
|
||||
|
||||
# def kit_integrity_completion(self):
|
||||
# """
|
||||
# Performs check of imported reagents
|
||||
# NOTE: this will not change self.reagents which should be fine
|
||||
# since it's only used when looking up
|
||||
# """
|
||||
# # from .main_window_functions import kit_integrity_completion_function
|
||||
# self = kit_integrity_completion_function(self)
|
||||
# self.result_reporter()
|
||||
|
||||
# def submit_new_sample(self):
|
||||
# """
|
||||
# Attempt to add sample to database when 'submit' button clicked
|
||||
# """
|
||||
# # from .main_window_functions import submit_new_sample_function
|
||||
# self = submit_new_sample_function(self)
|
||||
# self.result_reporter()
|
||||
|
||||
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, name:str|None=None):
|
||||
"""
|
||||
Action to create new reagent in DB.
|
||||
|
||||
Args:
|
||||
reagent_lot (str | None, optional): Parsed reagent from import form. Defaults to None.
|
||||
reagent_type (str | None, optional): Parsed reagent type from import form. Defaults to None.
|
||||
|
||||
Returns:
|
||||
models.Reagent: the constructed reagent object to add to submission
|
||||
"""
|
||||
report = Report()
|
||||
if isinstance(reagent_lot, bool):
|
||||
reagent_lot = ""
|
||||
# create form
|
||||
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
|
||||
if dlg.exec():
|
||||
# extract form info
|
||||
# info = extract_form_info(dlg)
|
||||
info = dlg.parse_form()
|
||||
logger.debug(f"Reagent info: {info}")
|
||||
# create reagent object
|
||||
# reagent = construct_reagent(ctx=self.ctx, info_dict=info)
|
||||
reagent = PydReagent(ctx=self.ctx, **info)
|
||||
# send reagent to db
|
||||
# store_reagent(ctx=self.ctx, reagent=reagent)
|
||||
sqlobj, result = reagent.toSQL()
|
||||
sqlobj.save()
|
||||
# result = store_object(ctx=self.ctx, object=reagent.toSQL()[0])
|
||||
report.add_result(result)
|
||||
self.result_reporter()
|
||||
return reagent
|
||||
|
||||
# def generateReport(self):
|
||||
# """
|
||||
# Action to create a summary of sheet data per client
|
||||
# """
|
||||
# # from .main_window_functions import generate_report_function
|
||||
# self, result = generate_report_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def add_kit(self):
|
||||
# """
|
||||
# Constructs new kit from yaml and adds to DB.
|
||||
# """
|
||||
# # from .main_window_functions import add_kit_function
|
||||
# self, result = add_kit_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def add_org(self):
|
||||
# """
|
||||
# Constructs new kit from yaml and adds to DB.
|
||||
# """
|
||||
# # from .main_window_functions import add_org_function
|
||||
# self, result = add_org_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def _controls_getter(self):
|
||||
# """
|
||||
# Lookup controls from database and send to chartmaker
|
||||
# """
|
||||
# # from .main_window_functions import controls_getter_function
|
||||
# self = controls_getter_function(self)
|
||||
# self.result_reporter()
|
||||
|
||||
# def _chart_maker(self):
|
||||
# """
|
||||
# Creates plotly charts for webview
|
||||
# """
|
||||
# # from .main_window_functions import chart_maker_function
|
||||
# self = chart_maker_function(self)
|
||||
# self.result_reporter()
|
||||
|
||||
# def linkControls(self):
|
||||
# """
|
||||
# Adds controls pulled from irida to relevant submissions
|
||||
# NOTE: Depreciated due to improvements in controls scraper.
|
||||
# """
|
||||
# # from .main_window_functions import link_controls_function
|
||||
# self, result = link_controls_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def linkExtractions(self):
|
||||
# """
|
||||
# Links extraction logs from .csv files to relevant submissions.
|
||||
# """
|
||||
# # from .main_window_functions import link_extractions_function
|
||||
# self, result = link_extractions_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def linkPCR(self):
|
||||
# """
|
||||
# Links PCR logs from .csv files to relevant submissions.
|
||||
# """
|
||||
# # from .main_window_functions import link_pcr_function
|
||||
# self, result = link_pcr_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def importPCRResults(self):
|
||||
# """
|
||||
# Imports results exported from Design and Analysis .eds files
|
||||
# """
|
||||
# # from .main_window_functions import import_pcr_results_function
|
||||
# self, result = import_pcr_results_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def construct_first_strand(self):
|
||||
# """
|
||||
# Converts first strand excel sheet to Biomek CSV
|
||||
# """
|
||||
# from .main_window_functions import construct_first_strand_function
|
||||
# self, result = construct_first_strand_function(self)
|
||||
# self.result_reporter(result)
|
||||
|
||||
# def scrape_reagents(self, *args, **kwargs):
|
||||
# # from .main_window_functions import scrape_reagents
|
||||
# logger.debug(f"Args: {args}")
|
||||
# logger.debug(F"kwargs: {kwargs}")
|
||||
# self = scrape_reagents(self, args[0])
|
||||
# self.kit_integrity_completion()
|
||||
# self.result_reporter()
|
||||
|
||||
# def export_csv(self, fname:Path|None=None):
|
||||
# # from .main_window_functions import export_csv_function
|
||||
# export_csv_function(self, fname)
|
||||
|
||||
class AddSubForm(QWidget):
|
||||
|
||||
def __init__(self, parent:QWidget):
|
||||
logger.debug(f"Initializating subform...")
|
||||
super(QWidget, self).__init__(parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.parent = parent
|
||||
# Initialize tab screen
|
||||
self.tabs = QTabWidget()
|
||||
self.tab1 = QWidget()
|
||||
self.tab2 = QWidget()
|
||||
self.tab3 = QWidget()
|
||||
self.tabs.resize(300,200)
|
||||
# Add tabs
|
||||
self.tabs.addTab(self.tab1,"Submissions")
|
||||
self.tabs.addTab(self.tab2,"Controls")
|
||||
self.tabs.addTab(self.tab3, "Add Kit")
|
||||
# Create submission adder form
|
||||
self.formwidget = SubmissionFormContainer(self)
|
||||
self.formlayout = QVBoxLayout(self)
|
||||
self.formwidget.setLayout(self.formlayout)
|
||||
self.formwidget.setFixedWidth(300)
|
||||
# Make scrollable interior for form
|
||||
self.interior = QScrollArea(self.tab1)
|
||||
self.interior.setWidgetResizable(True)
|
||||
self.interior.setFixedWidth(325)
|
||||
self.interior.setWidget(self.formwidget)
|
||||
# Create sheet to hold existing submissions
|
||||
self.sheetwidget = QWidget(self)
|
||||
self.sheetlayout = QVBoxLayout(self)
|
||||
self.sheetwidget.setLayout(self.sheetlayout)
|
||||
self.sub_wid = SubmissionsSheet(parent=parent)
|
||||
self.sheetlayout.addWidget(self.sub_wid)
|
||||
# Create layout of first tab to hold form and sheet
|
||||
self.tab1.layout = QHBoxLayout(self)
|
||||
self.tab1.setLayout(self.tab1.layout)
|
||||
self.tab1.layout.addWidget(self.interior)
|
||||
self.tab1.layout.addWidget(self.sheetwidget)
|
||||
# create widgets for tab 2
|
||||
# self.datepicker = ControlsDatePicker()
|
||||
# self.webengineview = QWebEngineView()
|
||||
# set tab2 layout
|
||||
self.tab2.layout = QVBoxLayout(self)
|
||||
# self.control_typer = QComboBox()
|
||||
# fetch types of controls
|
||||
# con_types = get_all_Control_Types_names(ctx=parent.ctx)
|
||||
# con_types = [item.name for item in lookup_control_types(ctx=parent.ctx)]
|
||||
# con_types = [item.name for item in ControlType.query()]
|
||||
# self.control_typer.addItems(con_types)
|
||||
# create custom widget to get types of analysis
|
||||
# self.mode_typer = QComboBox()
|
||||
# mode_types = get_all_available_modes(ctx=parent.ctx)
|
||||
# mode_types = lookup_modes(ctx=parent.ctx)
|
||||
# mode_types = Control.get_modes()
|
||||
# self.mode_typer.addItems(mode_types)
|
||||
# create custom widget to get subtypes of analysis
|
||||
# self.sub_typer = QComboBox()
|
||||
# self.sub_typer.setEnabled(False)
|
||||
# add widgets to tab2 layout
|
||||
# self.tab2.layout.addWidget(self.datepicker)
|
||||
# self.tab2.layout.addWidget(self.control_typer)
|
||||
# self.tab2.layout.addWidget(self.mode_typer)
|
||||
# self.tab2.layout.addWidget(self.sub_typer)
|
||||
# self.tab2.layout.addWidget(self.webengineview)
|
||||
self.controls_viewer = ControlsViewer(self)
|
||||
self.tab2.layout.addWidget(self.controls_viewer)
|
||||
self.tab2.setLayout(self.tab2.layout)
|
||||
# create custom widget to add new tabs
|
||||
adder = KitAdder(self)
|
||||
self.tab3.layout = QVBoxLayout(self)
|
||||
self.tab3.layout.addWidget(adder)
|
||||
self.tab3.setLayout(self.tab3.layout)
|
||||
# add tabs to main widget
|
||||
self.layout.addWidget(self.tabs)
|
||||
self.setLayout(self.layout)
|
||||
193
src/submissions/frontend/widgets/controls_chart.py
Normal file
193
src/submissions/frontend/widgets/controls_chart.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
|
||||
QDateEdit, QLabel, QSizePolicy
|
||||
)
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend.db import ControlType, Control, get_control_subtypes
|
||||
from PyQt6.QtCore import QDate, QSize
|
||||
import logging
|
||||
from tools import Report, Result
|
||||
from backend.excel.reports import convert_data_list_to_df
|
||||
from frontend.visualizations.control_charts import create_charts, construct_html
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class ControlsViewer(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent)
|
||||
self.app = self.parent().parent
|
||||
print(f"\n\n{self.app}\n\n")
|
||||
self.report = Report()
|
||||
self.datepicker = ControlsDatePicker()
|
||||
self.webengineview = QWebEngineView()
|
||||
# set tab2 layout
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.control_typer = QComboBox()
|
||||
# fetch types of controls
|
||||
# con_types = get_all_Control_Types_names(ctx=parent.ctx)
|
||||
# con_types = [item.name for item in lookup_control_types(ctx=parent.ctx)]
|
||||
con_types = [item.name for item in ControlType.query()]
|
||||
self.control_typer.addItems(con_types)
|
||||
# create custom widget to get types of analysis
|
||||
self.mode_typer = QComboBox()
|
||||
# mode_types = get_all_available_modes(ctx=parent.ctx)
|
||||
# mode_types = lookup_modes(ctx=parent.ctx)
|
||||
mode_types = Control.get_modes()
|
||||
self.mode_typer.addItems(mode_types)
|
||||
# create custom widget to get subtypes of analysis
|
||||
self.sub_typer = QComboBox()
|
||||
self.sub_typer.setEnabled(False)
|
||||
# add widgets to tab2 layout
|
||||
self.layout.addWidget(self.datepicker)
|
||||
self.layout.addWidget(self.control_typer)
|
||||
self.layout.addWidget(self.mode_typer)
|
||||
self.layout.addWidget(self.sub_typer)
|
||||
self.layout.addWidget(self.webengineview)
|
||||
self.setLayout(self.layout)
|
||||
self.controls_getter()
|
||||
self.control_typer.currentIndexChanged.connect(self.controls_getter)
|
||||
self.mode_typer.currentIndexChanged.connect(self.controls_getter)
|
||||
self.datepicker.start_date.dateChanged.connect(self.controls_getter)
|
||||
self.datepicker.end_date.dateChanged.connect(self.controls_getter)
|
||||
|
||||
def controls_getter(self):
|
||||
"""
|
||||
Lookup controls from database and send to chartmaker
|
||||
"""
|
||||
# from .main_window_functions import controls_getter_function
|
||||
self.controls_getter_function()
|
||||
# self.result_reporter()
|
||||
|
||||
def chart_maker(self):
|
||||
"""
|
||||
Creates plotly charts for webview
|
||||
"""
|
||||
# from .main_window_functions import chart_maker_function
|
||||
self.chart_maker_function()
|
||||
# self.result_reporter()
|
||||
|
||||
def controls_getter_function(self):
|
||||
"""
|
||||
Get controls based on start/end dates
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
# subtype defaults to disabled
|
||||
try:
|
||||
self.sub_typer.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
# correct start date being more recent than end date and rerun
|
||||
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
||||
logger.warning("Start date after end date is not allowed!")
|
||||
threemonthsago = self.datepicker.end_date.date().addDays(-60)
|
||||
# block signal that will rerun controls getter and set start date
|
||||
# Without triggering this function again
|
||||
with QSignalBlocker(self.datepicker.start_date) as blocker:
|
||||
self.datepicker.start_date.setDate(threemonthsago)
|
||||
self.controls_getter()
|
||||
self.report.add_result(report)
|
||||
return
|
||||
# convert to python useable date objects
|
||||
self.start_date = self.datepicker.start_date.date().toPyDate()
|
||||
self.end_date = self.datepicker.end_date.date().toPyDate()
|
||||
self.con_type = self.control_typer.currentText()
|
||||
self.mode = self.mode_typer.currentText()
|
||||
self.sub_typer.clear()
|
||||
# lookup subtypes
|
||||
sub_types = get_control_subtypes(type=self.con_type, mode=self.mode)
|
||||
# sub_types = lookup_controls(ctx=obj.ctx, control_type=obj.con_type)
|
||||
if sub_types != []:
|
||||
# block signal that will rerun controls getter and update sub_typer
|
||||
with QSignalBlocker(self.sub_typer) as blocker:
|
||||
self.sub_typer.addItems(sub_types)
|
||||
self.sub_typer.setEnabled(True)
|
||||
self.sub_typer.currentTextChanged.connect(self.chart_maker)
|
||||
else:
|
||||
self.sub_typer.clear()
|
||||
self.sub_typer.setEnabled(False)
|
||||
self.chart_maker()
|
||||
self.report.add_result(report)
|
||||
|
||||
|
||||
def chart_maker_function(self):
|
||||
"""
|
||||
Create html chart for controls reporting
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart Date: {self.start_date}\n\tEnd Date: {self.end_date}")
|
||||
# set the subtype for kraken
|
||||
if self.sub_typer.currentText() == "":
|
||||
self.subtype = None
|
||||
else:
|
||||
self.subtype = self.sub_typer.currentText()
|
||||
logger.debug(f"Subtype: {self.subtype}")
|
||||
# query all controls using the type/start and end dates from the gui
|
||||
# controls = get_all_controls_by_type(ctx=obj.ctx, con_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
|
||||
# controls = lookup_controls(ctx=obj.ctx, control_type=obj.con_type, start_date=obj.start_date, end_date=obj.end_date)
|
||||
controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
|
||||
# if no data found from query set fig to none for reporting in webview
|
||||
if controls == None:
|
||||
fig = None
|
||||
else:
|
||||
# change each control to list of dictionaries
|
||||
data = [control.convert_by_mode(mode=self.mode) for control in controls]
|
||||
# flatten data to one dimensional list
|
||||
data = [item for sublist in data for item in sublist]
|
||||
logger.debug(f"Control objects going into df conversion: {type(data)}")
|
||||
if data == []:
|
||||
self.report.add_result(Result(status="Critical", msg="No data found for controls in given date range."))
|
||||
return
|
||||
# send to dataframe creator
|
||||
df = convert_data_list_to_df(input=data, subtype=self.subtype)
|
||||
if self.subtype == None:
|
||||
title = self.mode
|
||||
else:
|
||||
title = f"{self.mode} - {self.subtype}"
|
||||
# send dataframe to chart maker
|
||||
fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title)
|
||||
logger.debug(f"Updating figure...")
|
||||
# construct html for webview
|
||||
html = construct_html(figure=fig)
|
||||
logger.debug(f"The length of html code is: {len(html)}")
|
||||
self.webengineview.setHtml(html)
|
||||
self.webengineview.update()
|
||||
logger.debug("Figure updated... I hope.")
|
||||
self.report.add_result(report)
|
||||
|
||||
class ControlsDatePicker(QWidget):
|
||||
"""
|
||||
custom widget to pick start and end dates for controls graphs
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.start_date = QDateEdit(calendarPopup=True)
|
||||
# start date is two months prior to end date by default
|
||||
twomonthsago = QDate.currentDate().addDays(-60)
|
||||
self.start_date.setDate(twomonthsago)
|
||||
self.end_date = QDateEdit(calendarPopup=True)
|
||||
self.end_date.setDate(QDate.currentDate())
|
||||
self.layout = QHBoxLayout()
|
||||
self.layout.addWidget(QLabel("Start Date"))
|
||||
self.layout.addWidget(self.start_date)
|
||||
self.layout.addWidget(QLabel("End Date"))
|
||||
self.layout.addWidget(self.end_date)
|
||||
self.setLayout(self.layout)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return QSize(80,20)
|
||||
56
src/submissions/frontend/widgets/functions.py
Normal file
56
src/submissions/frontend/widgets/functions.py
Normal file
@@ -0,0 +1,56 @@
|
||||
'''
|
||||
functions used by all windows in the application's frontend
|
||||
NOTE: Depreciated. Moved to functions.__init__
|
||||
'''
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from PyQt6.QtWidgets import QMainWindow, QFileDialog
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
def select_open_file(obj:QMainWindow, file_extension:str) -> Path:
|
||||
"""
|
||||
File dialog to select a file to read from
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window to be parent
|
||||
file_extension (str): file extension
|
||||
|
||||
Returns:
|
||||
Path: Path of file to be opened
|
||||
"""
|
||||
try:
|
||||
# home_dir = Path(obj.ctx.directory_path).resolve().__str__()
|
||||
home_dir = obj.last_dir.resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().resolve().__str__()
|
||||
except AttributeError:
|
||||
home_dir = obj.app.last_dir.resolve().__str__()
|
||||
fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0])
|
||||
# fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', filter = f"{file_extension}(*.{file_extension})")[0])
|
||||
obj.last_file = fname
|
||||
return fname
|
||||
|
||||
def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path:
|
||||
"""
|
||||
File dialog to select a file to write to
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): Original main app window to be parent
|
||||
default_name (str): default base file name
|
||||
extension (str): file extension
|
||||
|
||||
Returns:
|
||||
Path: Path of file to be opened
|
||||
"""
|
||||
try:
|
||||
# home_dir = Path(obj.ctx.directory_path).joinpath(default_name).resolve().__str__()
|
||||
home_dir = obj.last_dir.joinpath(default_name).resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().joinpath(default_name).resolve().__str__()
|
||||
except AttributeError:
|
||||
home_dir = obj.app.last_dir.joinpath(default_name).resolve().__str__()
|
||||
fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter = f"{extension}(*.{extension})")[0])
|
||||
# fname = Path(QFileDialog.getSaveFileName(obj, "Save File", filter = f"{extension}(*.{extension})")[0])
|
||||
obj.last_dir = fname.parent
|
||||
return fname
|
||||
223
src/submissions/frontend/widgets/kit_creator.py
Normal file
223
src/submissions/frontend/widgets/kit_creator.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QScrollArea,
|
||||
QGridLayout, QPushButton, QLabel,
|
||||
QLineEdit, QComboBox, QDoubleSpinBox,
|
||||
QSpinBox, QDateEdit
|
||||
)
|
||||
from sqlalchemy import FLOAT, INTEGER
|
||||
from backend.db import SubmissionTypeKitTypeAssociation, SubmissionType, ReagentType
|
||||
from backend.validators import PydReagentType, PydKit
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from tools import Report, Result
|
||||
from typing import Tuple
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class KitAdder(QWidget):
|
||||
"""
|
||||
dialog to get information to add kit
|
||||
"""
|
||||
def __init__(self, parent) -> None:
|
||||
super().__init__(parent)
|
||||
# self.ctx = parent_ctx
|
||||
self.report = Report()
|
||||
self.app = parent.parent
|
||||
main_box = QVBoxLayout(self)
|
||||
scroll = QScrollArea(self)
|
||||
main_box.addWidget(scroll)
|
||||
scroll.setWidgetResizable(True)
|
||||
scrollContent = QWidget(scroll)
|
||||
self.grid = QGridLayout()
|
||||
# self.setLayout(self.grid)
|
||||
scrollContent.setLayout(self.grid)
|
||||
# insert submit button at top
|
||||
self.submit_btn = QPushButton("Submit")
|
||||
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
||||
self.grid.addWidget(QLabel("Kit Name:"),2,0)
|
||||
# widget to get kit name
|
||||
kit_name = QLineEdit()
|
||||
kit_name.setObjectName("kit_name")
|
||||
self.grid.addWidget(kit_name,2,1)
|
||||
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||
# widget to get uses of kit
|
||||
used_for = QComboBox()
|
||||
used_for.setObjectName("used_for")
|
||||
# Insert all existing sample types
|
||||
# used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
|
||||
used_for.addItems([item.name for item in SubmissionType.query()])
|
||||
used_for.setEditable(True)
|
||||
self.grid.addWidget(used_for,3,1)
|
||||
# Get all fields in SubmissionTypeKitTypeAssociation
|
||||
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
|
||||
for iii, column in enumerate(self.columns):
|
||||
idx = iii + 4
|
||||
# convert field name to human readable.
|
||||
field_name = column.name.replace("_", " ").title()
|
||||
self.grid.addWidget(QLabel(field_name),idx,0)
|
||||
match column.type:
|
||||
case FLOAT():
|
||||
add_widget = QDoubleSpinBox()
|
||||
add_widget.setMinimum(0)
|
||||
add_widget.setMaximum(9999)
|
||||
case INTEGER():
|
||||
add_widget = QSpinBox()
|
||||
add_widget.setMinimum(0)
|
||||
add_widget.setMaximum(9999)
|
||||
case _:
|
||||
add_widget = QLineEdit()
|
||||
add_widget.setObjectName(column.name)
|
||||
self.grid.addWidget(add_widget, idx,1)
|
||||
self.add_RT_btn = QPushButton("Add Reagent Type")
|
||||
self.grid.addWidget(self.add_RT_btn)
|
||||
self.add_RT_btn.clicked.connect(self.add_RT)
|
||||
self.submit_btn.clicked.connect(self.submit)
|
||||
scroll.setWidget(scrollContent)
|
||||
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
"qt_scrollarea_vcontainer", "submit_btn"
|
||||
]
|
||||
|
||||
def add_RT(self) -> None:
|
||||
"""
|
||||
insert new reagent type row
|
||||
"""
|
||||
print(self.app)
|
||||
# get bottommost row
|
||||
maxrow = self.grid.rowCount()
|
||||
reg_form = ReagentTypeForm()
|
||||
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||
self.grid.addWidget(reg_form, maxrow,0,1,4)
|
||||
|
||||
def submit(self) -> None:
|
||||
"""
|
||||
send kit to database
|
||||
"""
|
||||
report = Report()
|
||||
# get form info
|
||||
info, reagents = self.parse_form()
|
||||
# info, reagents = extract_form_info(self)
|
||||
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
|
||||
logger.debug(f"kit info: {pformat(info)}")
|
||||
logger.debug(f"kit reagents: {pformat(reagents)}")
|
||||
info['reagent_types'] = reagents
|
||||
logger.debug(pformat(info))
|
||||
# send to kit constructor
|
||||
kit = PydKit(name=info['kit_name'])
|
||||
for reagent in info['reagent_types']:
|
||||
uses = {
|
||||
info['used_for']:
|
||||
{'sheet':reagent['sheet'],
|
||||
'name':reagent['name'],
|
||||
'lot':reagent['lot'],
|
||||
'expiry':reagent['expiry']
|
||||
}}
|
||||
kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
|
||||
logger.debug(f"Output pyd object: {kit.__dict__}")
|
||||
# result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
|
||||
sqlobj, result = kit.toSQL(self.ctx)
|
||||
report.add_result(result=result)
|
||||
sqlobj.save()
|
||||
|
||||
self.__init__(self.parent())
|
||||
|
||||
def parse_form(self) -> Tuple[dict, list]:
|
||||
logger.debug(f"Hello from {self.__class__} parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
|
||||
for widget in widgets:
|
||||
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||
match widget:
|
||||
case ReagentTypeForm():
|
||||
reagents.append(widget.parse_form())
|
||||
case QLineEdit():
|
||||
info[widget.objectName()] = widget.text()
|
||||
case QComboBox():
|
||||
info[widget.objectName()] = widget.currentText()
|
||||
case QDateEdit():
|
||||
info[widget.objectName()] = widget.date().toPyDate()
|
||||
return info, reagents
|
||||
|
||||
class ReagentTypeForm(QWidget):
|
||||
"""
|
||||
custom widget to add information about a new reagenttype
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
grid = QGridLayout()
|
||||
self.setLayout(grid)
|
||||
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||
# Widget to get reagent info
|
||||
self.reagent_getter = QComboBox()
|
||||
self.reagent_getter.setObjectName("rtname")
|
||||
# lookup all reagent type names from db
|
||||
# lookup = lookup_reagent_types(ctx=ctx)
|
||||
lookup = ReagentType.query()
|
||||
logger.debug(f"Looked up ReagentType names: {lookup}")
|
||||
self.reagent_getter.addItems([item.__str__() for item in lookup])
|
||||
self.reagent_getter.setEditable(True)
|
||||
grid.addWidget(self.reagent_getter,0,1)
|
||||
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
|
||||
# widget to get extension of life
|
||||
self.eol = QSpinBox()
|
||||
self.eol.setObjectName('eol')
|
||||
self.eol.setMinimum(0)
|
||||
grid.addWidget(self.eol, 0,3)
|
||||
grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
|
||||
self.location_sheet_name = QLineEdit()
|
||||
self.location_sheet_name.setObjectName("sheet")
|
||||
self.location_sheet_name.setText("e.g. 'Reagent Info'")
|
||||
grid.addWidget(self.location_sheet_name, 1,1)
|
||||
for iii, item in enumerate(["Name", "Lot", "Expiry"]):
|
||||
idx = iii + 2
|
||||
grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
|
||||
row = QSpinBox()
|
||||
row.setFixedWidth(50)
|
||||
row.setObjectName(f'{item.lower()}_row')
|
||||
row.setMinimum(0)
|
||||
grid.addWidget(row, idx, 1)
|
||||
grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
|
||||
col = QSpinBox()
|
||||
col.setFixedWidth(50)
|
||||
col.setObjectName(f'{item.lower()}_column')
|
||||
col.setMinimum(0)
|
||||
grid.addWidget(col, idx, 3)
|
||||
self.setFixedHeight(175)
|
||||
max_row = grid.rowCount()
|
||||
self.r_button = QPushButton("Remove")
|
||||
self.r_button.clicked.connect(self.remove)
|
||||
grid.addWidget(self.r_button,max_row,0,1,1)
|
||||
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
"qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
|
||||
]
|
||||
|
||||
def remove(self):
|
||||
self.setParent(None)
|
||||
self.destroy()
|
||||
|
||||
def parse_form(self) -> dict:
|
||||
logger.debug(f"Hello from {self.__class__} parser!")
|
||||
info = {}
|
||||
info['eol'] = self.eol.value()
|
||||
info['sheet'] = self.location_sheet_name.text()
|
||||
info['rtname'] = self.reagent_getter.currentText()
|
||||
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
|
||||
for widget in widgets:
|
||||
logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||
match widget:
|
||||
case QLineEdit():
|
||||
info[widget.objectName()] = widget.text()
|
||||
case QComboBox():
|
||||
info[widget.objectName()] = widget.currentText()
|
||||
case QDateEdit():
|
||||
info[widget.objectName()] = widget.date().toPyDate()
|
||||
case QSpinBox() | QDoubleSpinBox():
|
||||
if "_" in widget.objectName():
|
||||
key, sub_key = widget.objectName().split("_")
|
||||
if key not in info.keys():
|
||||
info[key] = {}
|
||||
logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
|
||||
info[key][sub_key] = widget.value()
|
||||
return info
|
||||
749
src/submissions/frontend/widgets/misc.py
Normal file
749
src/submissions/frontend/widgets/misc.py
Normal file
@@ -0,0 +1,749 @@
|
||||
'''
|
||||
Contains miscellaneous widgets for frontend functions
|
||||
'''
|
||||
from datetime import date
|
||||
from pprint import pformat
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout,
|
||||
QLineEdit, QComboBox, QDialog,
|
||||
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
|
||||
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
|
||||
QHBoxLayout, QScrollArea, QFormLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QDate, QSize, pyqtSignal
|
||||
from tools import check_not_nan, jinja_template_loading, Settings, Result
|
||||
from backend.db.models import *
|
||||
from sqlalchemy import FLOAT, INTEGER
|
||||
import logging
|
||||
import numpy as np
|
||||
from .pop_ups import AlertPop, QuestionAsker
|
||||
from backend.validators import PydReagent, PydKit, PydReagentType, PydSubmission
|
||||
from typing import Tuple, List
|
||||
from pprint import pformat
|
||||
import difflib
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
env = jinja_template_loading()
|
||||
|
||||
class AddReagentForm(QDialog):
|
||||
"""
|
||||
dialog to add gather info about new reagent
|
||||
"""
|
||||
def __init__(self, ctx:dict, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, reagent_name:str|None=None) -> None:
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
if reagent_lot == None:
|
||||
reagent_lot = reagent_type
|
||||
|
||||
self.setWindowTitle("Add Reagent")
|
||||
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
# widget to get lot info
|
||||
self.name_input = QComboBox()
|
||||
self.name_input.setObjectName("name")
|
||||
self.name_input.setEditable(True)
|
||||
self.name_input.setCurrentText(reagent_name)
|
||||
# self.name_input.setText(reagent_name)
|
||||
self.lot_input = QLineEdit()
|
||||
self.lot_input.setObjectName("lot")
|
||||
self.lot_input.setText(reagent_lot)
|
||||
# widget to get expiry info
|
||||
self.exp_input = QDateEdit(calendarPopup=True)
|
||||
self.exp_input.setObjectName('expiry')
|
||||
# if expiry is not passed in from gui, use today
|
||||
if expiry == None:
|
||||
self.exp_input.setDate(QDate.currentDate())
|
||||
else:
|
||||
self.exp_input.setDate(expiry)
|
||||
# widget to get reagent type info
|
||||
self.type_input = QComboBox()
|
||||
self.type_input.setObjectName('type')
|
||||
# self.type_input.addItems([item.name for item in lookup_reagent_types(ctx=ctx)])
|
||||
self.type_input.addItems([item.name for item in ReagentType.query()])
|
||||
logger.debug(f"Trying to find index of {reagent_type}")
|
||||
# convert input to user friendly string?
|
||||
try:
|
||||
reagent_type = reagent_type.replace("_", " ").title()
|
||||
except AttributeError:
|
||||
reagent_type = None
|
||||
# set parsed reagent type to top of list
|
||||
index = self.type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
|
||||
if index >= 0:
|
||||
self.type_input.setCurrentIndex(index)
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(QLabel("Name:"))
|
||||
self.layout.addWidget(self.name_input)
|
||||
self.layout.addWidget(QLabel("Lot:"))
|
||||
self.layout.addWidget(self.lot_input)
|
||||
self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
|
||||
self.layout.addWidget(self.exp_input)
|
||||
self.layout.addWidget(QLabel("Type:"))
|
||||
self.layout.addWidget(self.type_input)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
self.type_input.currentTextChanged.connect(self.update_names)
|
||||
|
||||
def parse_form(self):
|
||||
return dict(name=self.name_input.currentText(),
|
||||
lot=self.lot_input.text(),
|
||||
expiry=self.exp_input.date().toPyDate(),
|
||||
type=self.type_input.currentText())
|
||||
|
||||
def update_names(self):
|
||||
"""
|
||||
Updates reagent names form field with examples from reagent type
|
||||
"""
|
||||
logger.debug(self.type_input.currentText())
|
||||
self.name_input.clear()
|
||||
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=self.type_input.currentText())
|
||||
lookup = Reagent.query(reagent_type=self.type_input.currentText())
|
||||
self.name_input.addItems(list(set([item.name for item in lookup])))
|
||||
|
||||
class ReportDatePicker(QDialog):
|
||||
"""
|
||||
custom dialog to ask for report start/stop dates
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Select Report Date Range")
|
||||
# make confirm/reject buttons
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
# widgets to ask for dates
|
||||
self.start_date = QDateEdit(calendarPopup=True)
|
||||
self.start_date.setObjectName("start_date")
|
||||
self.start_date.setDate(QDate.currentDate())
|
||||
self.end_date = QDateEdit(calendarPopup=True)
|
||||
self.end_date.setObjectName("end_date")
|
||||
self.end_date.setDate(QDate.currentDate())
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout.addWidget(QLabel("Start Date"))
|
||||
self.layout.addWidget(self.start_date)
|
||||
self.layout.addWidget(QLabel("End Date"))
|
||||
self.layout.addWidget(self.end_date)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self):
|
||||
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
||||
|
||||
# class KitAdder(QWidget):
|
||||
# """
|
||||
# dialog to get information to add kit
|
||||
# """
|
||||
# def __init__(self) -> None:
|
||||
# super().__init__()
|
||||
# # self.ctx = parent_ctx
|
||||
# main_box = QVBoxLayout(self)
|
||||
# scroll = QScrollArea(self)
|
||||
# main_box.addWidget(scroll)
|
||||
# scroll.setWidgetResizable(True)
|
||||
# scrollContent = QWidget(scroll)
|
||||
# self.grid = QGridLayout()
|
||||
# # self.setLayout(self.grid)
|
||||
# scrollContent.setLayout(self.grid)
|
||||
# # insert submit button at top
|
||||
# self.submit_btn = QPushButton("Submit")
|
||||
# self.grid.addWidget(self.submit_btn,0,0,1,1)
|
||||
# self.grid.addWidget(QLabel("Kit Name:"),2,0)
|
||||
# # widget to get kit name
|
||||
# kit_name = QLineEdit()
|
||||
# kit_name.setObjectName("kit_name")
|
||||
# self.grid.addWidget(kit_name,2,1)
|
||||
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||
# # widget to get uses of kit
|
||||
# used_for = QComboBox()
|
||||
# used_for.setObjectName("used_for")
|
||||
# # Insert all existing sample types
|
||||
# # used_for.addItems([item.name for item in lookup_submission_type(ctx=parent_ctx)])
|
||||
# used_for.addItems([item.name for item in SubmissionType.query()])
|
||||
# used_for.setEditable(True)
|
||||
# self.grid.addWidget(used_for,3,1)
|
||||
# # Get all fields in SubmissionTypeKitTypeAssociation
|
||||
# self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
|
||||
# for iii, column in enumerate(self.columns):
|
||||
# idx = iii + 4
|
||||
# # convert field name to human readable.
|
||||
# field_name = column.name.replace("_", " ").title()
|
||||
# self.grid.addWidget(QLabel(field_name),idx,0)
|
||||
# match column.type:
|
||||
# case FLOAT():
|
||||
# add_widget = QDoubleSpinBox()
|
||||
# add_widget.setMinimum(0)
|
||||
# add_widget.setMaximum(9999)
|
||||
# case INTEGER():
|
||||
# add_widget = QSpinBox()
|
||||
# add_widget.setMinimum(0)
|
||||
# add_widget.setMaximum(9999)
|
||||
# case _:
|
||||
# add_widget = QLineEdit()
|
||||
# add_widget.setObjectName(column.name)
|
||||
# self.grid.addWidget(add_widget, idx,1)
|
||||
# self.add_RT_btn = QPushButton("Add Reagent Type")
|
||||
# self.grid.addWidget(self.add_RT_btn)
|
||||
# self.add_RT_btn.clicked.connect(self.add_RT)
|
||||
# self.submit_btn.clicked.connect(self.submit)
|
||||
# scroll.setWidget(scrollContent)
|
||||
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
# "qt_scrollarea_vcontainer", "submit_btn"
|
||||
# ]
|
||||
|
||||
# def add_RT(self) -> None:
|
||||
# """
|
||||
# insert new reagent type row
|
||||
# """
|
||||
# # get bottommost row
|
||||
# maxrow = self.grid.rowCount()
|
||||
# reg_form = ReagentTypeForm()
|
||||
# reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||
# # self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||
# self.grid.addWidget(reg_form, maxrow,0,1,4)
|
||||
|
||||
|
||||
|
||||
# def submit(self) -> None:
|
||||
# """
|
||||
# send kit to database
|
||||
# """
|
||||
# # get form info
|
||||
# info, reagents = self.parse_form()
|
||||
# # info, reagents = extract_form_info(self)
|
||||
# info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
|
||||
# logger.debug(f"kit info: {pformat(info)}")
|
||||
# logger.debug(f"kit reagents: {pformat(reagents)}")
|
||||
# info['reagent_types'] = reagents
|
||||
# logger.debug(pformat(info))
|
||||
# # send to kit constructor
|
||||
# kit = PydKit(name=info['kit_name'])
|
||||
# for reagent in info['reagent_types']:
|
||||
# uses = {
|
||||
# info['used_for']:
|
||||
# {'sheet':reagent['sheet'],
|
||||
# 'name':reagent['name'],
|
||||
# 'lot':reagent['lot'],
|
||||
# 'expiry':reagent['expiry']
|
||||
# }}
|
||||
# kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
|
||||
# logger.debug(f"Output pyd object: {kit.__dict__}")
|
||||
# # result = construct_kit_from_yaml(ctx=self.ctx, kit_dict=info)
|
||||
# sqlobj, result = kit.toSQL(self.ctx)
|
||||
# sqlobj.save()
|
||||
# msg = AlertPop(message=result['message'], status=result['status'])
|
||||
# msg.exec()
|
||||
# self.__init__(self.ctx)
|
||||
|
||||
# def parse_form(self) -> Tuple[dict, list]:
|
||||
# logger.debug(f"Hello from {self.__class__} parser!")
|
||||
# info = {}
|
||||
# reagents = []
|
||||
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)]
|
||||
# for widget in widgets:
|
||||
# # logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||
# match widget:
|
||||
# case ReagentTypeForm():
|
||||
# reagents.append(widget.parse_form())
|
||||
# case QLineEdit():
|
||||
# info[widget.objectName()] = widget.text()
|
||||
# case QComboBox():
|
||||
# info[widget.objectName()] = widget.currentText()
|
||||
# case QDateEdit():
|
||||
# info[widget.objectName()] = widget.date().toPyDate()
|
||||
# return info, reagents
|
||||
|
||||
# class ReagentTypeForm(QWidget):
|
||||
# """
|
||||
# custom widget to add information about a new reagenttype
|
||||
# """
|
||||
# def __init__(self) -> None:
|
||||
# super().__init__()
|
||||
# grid = QGridLayout()
|
||||
# self.setLayout(grid)
|
||||
# grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||
# # Widget to get reagent info
|
||||
# self.reagent_getter = QComboBox()
|
||||
# self.reagent_getter.setObjectName("rtname")
|
||||
# # lookup all reagent type names from db
|
||||
# # lookup = lookup_reagent_types(ctx=ctx)
|
||||
# lookup = ReagentType.query()
|
||||
# logger.debug(f"Looked up ReagentType names: {lookup}")
|
||||
# self.reagent_getter.addItems([item.__str__() for item in lookup])
|
||||
# self.reagent_getter.setEditable(True)
|
||||
# grid.addWidget(self.reagent_getter,0,1)
|
||||
# grid.addWidget(QLabel("Extension of Life (months):"),0,2)
|
||||
# # widget to get extension of life
|
||||
# self.eol = QSpinBox()
|
||||
# self.eol.setObjectName('eol')
|
||||
# self.eol.setMinimum(0)
|
||||
# grid.addWidget(self.eol, 0,3)
|
||||
# grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
|
||||
# self.location_sheet_name = QLineEdit()
|
||||
# self.location_sheet_name.setObjectName("sheet")
|
||||
# self.location_sheet_name.setText("e.g. 'Reagent Info'")
|
||||
# grid.addWidget(self.location_sheet_name, 1,1)
|
||||
# for iii, item in enumerate(["Name", "Lot", "Expiry"]):
|
||||
# idx = iii + 2
|
||||
# grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
|
||||
# row = QSpinBox()
|
||||
# row.setFixedWidth(50)
|
||||
# row.setObjectName(f'{item.lower()}_row')
|
||||
# row.setMinimum(0)
|
||||
# grid.addWidget(row, idx, 1)
|
||||
# grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
|
||||
# col = QSpinBox()
|
||||
# col.setFixedWidth(50)
|
||||
# col.setObjectName(f'{item.lower()}_column')
|
||||
# col.setMinimum(0)
|
||||
# grid.addWidget(col, idx, 3)
|
||||
# self.setFixedHeight(175)
|
||||
# max_row = grid.rowCount()
|
||||
# self.r_button = QPushButton("Remove")
|
||||
# self.r_button.clicked.connect(self.remove)
|
||||
# grid.addWidget(self.r_button,max_row,0,1,1)
|
||||
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
# "qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
|
||||
# ]
|
||||
|
||||
# def remove(self):
|
||||
# self.setParent(None)
|
||||
# self.destroy()
|
||||
|
||||
# def parse_form(self) -> dict:
|
||||
# logger.debug(f"Hello from {self.__class__} parser!")
|
||||
# info = {}
|
||||
# info['eol'] = self.eol.value()
|
||||
# info['sheet'] = self.location_sheet_name.text()
|
||||
# info['rtname'] = self.reagent_getter.currentText()
|
||||
# widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
|
||||
# for widget in widgets:
|
||||
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
|
||||
# match widget:
|
||||
# case QLineEdit():
|
||||
# info[widget.objectName()] = widget.text()
|
||||
# case QComboBox():
|
||||
# info[widget.objectName()] = widget.currentText()
|
||||
# case QDateEdit():
|
||||
# info[widget.objectName()] = widget.date().toPyDate()
|
||||
# case QSpinBox() | QDoubleSpinBox():
|
||||
# if "_" in widget.objectName():
|
||||
# key, sub_key = widget.objectName().split("_")
|
||||
# if key not in info.keys():
|
||||
# info[key] = {}
|
||||
# logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
|
||||
# info[key][sub_key] = widget.value()
|
||||
# return info
|
||||
|
||||
# class ControlsDatePicker(QWidget):
|
||||
# """
|
||||
# custom widget to pick start and end dates for controls graphs
|
||||
# """
|
||||
# def __init__(self) -> None:
|
||||
# super().__init__()
|
||||
|
||||
# self.start_date = QDateEdit(calendarPopup=True)
|
||||
# # start date is two months prior to end date by default
|
||||
# twomonthsago = QDate.currentDate().addDays(-60)
|
||||
# self.start_date.setDate(twomonthsago)
|
||||
# self.end_date = QDateEdit(calendarPopup=True)
|
||||
# self.end_date.setDate(QDate.currentDate())
|
||||
# self.layout = QHBoxLayout()
|
||||
# self.layout.addWidget(QLabel("Start Date"))
|
||||
# self.layout.addWidget(self.start_date)
|
||||
# self.layout.addWidget(QLabel("End Date"))
|
||||
# self.layout.addWidget(self.end_date)
|
||||
# self.setLayout(self.layout)
|
||||
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
# def sizeHint(self) -> QSize:
|
||||
# return QSize(80,20)
|
||||
|
||||
class FirstStrandSalvage(QDialog):
|
||||
|
||||
def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None:
|
||||
super().__init__()
|
||||
if rsl_plate_num == None:
|
||||
rsl_plate_num = ""
|
||||
self.setWindowTitle("Add Reagent")
|
||||
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
self.submitter_id_input = QLineEdit()
|
||||
self.submitter_id_input.setText(submitter_id)
|
||||
self.rsl_plate_num = QLineEdit()
|
||||
self.rsl_plate_num.setText(rsl_plate_num)
|
||||
self.row_letter = QComboBox()
|
||||
self.row_letter.addItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
|
||||
self.row_letter.setEditable(False)
|
||||
self.column_number = QComboBox()
|
||||
self.column_number.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'])
|
||||
self.column_number.setEditable(False)
|
||||
self.layout = QFormLayout()
|
||||
self.layout.addRow(self.tr("&Sample Number:"), self.submitter_id_input)
|
||||
self.layout.addRow(self.tr("&Plate Number:"), self.rsl_plate_num)
|
||||
self.layout.addRow(self.tr("&Source Row:"), self.row_letter)
|
||||
self.layout.addRow(self.tr("&Source Column:"), self.column_number)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self):
|
||||
return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}")
|
||||
|
||||
class FirstStrandPlateList(QDialog):
|
||||
|
||||
def __init__(self, ctx:Settings) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle("First Strand Plates")
|
||||
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
# ww = [item.rsl_plate_num for item in lookup_submissions(ctx=ctx, submission_type="Wastewater")]
|
||||
ww = [item.rsl_plate_num for item in BasicSubmission.query(submission_type="Wastewater")]
|
||||
self.plate1 = QComboBox()
|
||||
self.plate2 = QComboBox()
|
||||
self.plate3 = QComboBox()
|
||||
self.layout = QFormLayout()
|
||||
for ii, plate in enumerate([self.plate1, self.plate2, self.plate3]):
|
||||
plate.addItems(ww)
|
||||
self.layout.addRow(self.tr(f"&Plate {ii+1}:"), plate)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self):
|
||||
output = []
|
||||
for plate in [self.plate1, self.plate2, self.plate3]:
|
||||
output.append(plate.currentText())
|
||||
return output
|
||||
|
||||
# class ReagentFormWidget(QWidget):
|
||||
|
||||
# def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
|
||||
# super().__init__(parent)
|
||||
# # self.setParent(parent)
|
||||
# self.reagent = reagent
|
||||
# self.extraction_kit = extraction_kit
|
||||
# # self.ctx = reagent.ctx
|
||||
# layout = QVBoxLayout()
|
||||
# self.label = self.ReagentParsedLabel(reagent=reagent)
|
||||
# layout.addWidget(self.label)
|
||||
# self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
||||
# layout.addWidget(self.lot)
|
||||
# # Remove spacing between reagents
|
||||
# layout.setContentsMargins(0,0,0,0)
|
||||
# self.setLayout(layout)
|
||||
# self.setObjectName(reagent.name)
|
||||
# self.missing = reagent.missing
|
||||
# # If changed set self.missing to True and update self.label
|
||||
# self.lot.currentTextChanged.connect(self.updated)
|
||||
|
||||
# def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||
# lot = self.lot.currentText()
|
||||
# # wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
|
||||
# wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
||||
# # if reagent doesn't exist in database, off to add it (uses App.add_reagent)
|
||||
# if wanted_reagent == None:
|
||||
# dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
|
||||
# if dlg.exec():
|
||||
# wanted_reagent = self.parent().parent().parent().parent().parent().parent().parent().parent().parent.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
|
||||
# return wanted_reagent, None
|
||||
# else:
|
||||
# # In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||
# logger.debug("Will not add reagent.")
|
||||
# return None, Result(msg="Failed integrity check", status="Critical")
|
||||
# else:
|
||||
# # Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
||||
# # from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
||||
# # rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
|
||||
# # rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
# rt = ReagentType.query(name=self.reagent.type)
|
||||
# if rt == None:
|
||||
# # rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
# rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
# return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
|
||||
|
||||
# def updated(self):
|
||||
# self.missing = True
|
||||
# self.label.updated(self.reagent.type)
|
||||
|
||||
|
||||
# class ReagentParsedLabel(QLabel):
|
||||
|
||||
# def __init__(self, reagent:PydReagent):
|
||||
# super().__init__()
|
||||
# try:
|
||||
# check = not reagent.missing
|
||||
# except:
|
||||
# check = False
|
||||
# self.setObjectName(f"{reagent.type}_label")
|
||||
# if check:
|
||||
# self.setText(f"Parsed {reagent.type}")
|
||||
# else:
|
||||
# self.setText(f"MISSING {reagent.type}")
|
||||
|
||||
# def updated(self, reagent_type:str):
|
||||
# self.setText(f"UPDATED {reagent_type}")
|
||||
|
||||
# class ReagentLot(QComboBox):
|
||||
|
||||
# def __init__(self, reagent, extraction_kit:str) -> None:
|
||||
# super().__init__()
|
||||
# # self.ctx = reagent.ctx
|
||||
# self.setEditable(True)
|
||||
# # if reagent.parsed:
|
||||
# # pass
|
||||
# logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
|
||||
# # below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
|
||||
# # lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type)
|
||||
# lookup = Reagent.query(reagent_type=reagent.type)
|
||||
# relevant_reagents = [item.__str__() for item in lookup]
|
||||
# output_reg = []
|
||||
# for rel_reagent in relevant_reagents:
|
||||
# # extract strings from any sets.
|
||||
# if isinstance(rel_reagent, set):
|
||||
# for thing in rel_reagent:
|
||||
# output_reg.append(thing)
|
||||
# elif isinstance(rel_reagent, str):
|
||||
# output_reg.append(rel_reagent)
|
||||
# relevant_reagents = output_reg
|
||||
# # if reagent in sheet is not found insert it into the front of relevant reagents so it shows
|
||||
# logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}")
|
||||
# if str(reagent.lot) not in relevant_reagents:
|
||||
# if check_not_nan(reagent.lot):
|
||||
# relevant_reagents.insert(0, str(reagent.lot))
|
||||
# else:
|
||||
# # TODO: look up the last used reagent of this type in the database
|
||||
# # looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
|
||||
# looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
|
||||
# try:
|
||||
# # looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
|
||||
# looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
||||
# except AttributeError:
|
||||
# looked_up_reg = None
|
||||
# logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
|
||||
# if looked_up_reg != None:
|
||||
# relevant_reagents.remove(str(looked_up_reg.lot))
|
||||
# relevant_reagents.insert(0, str(looked_up_reg.lot))
|
||||
# else:
|
||||
# if len(relevant_reagents) > 1:
|
||||
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
||||
# idx = relevant_reagents.index(str(reagent.lot))
|
||||
# logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}")
|
||||
# moved_reag = relevant_reagents.pop(idx)
|
||||
# relevant_reagents.insert(0, moved_reag)
|
||||
# else:
|
||||
# logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
|
||||
# logger.debug(f"New relevant reagents: {relevant_reagents}")
|
||||
# self.setObjectName(f"lot_{reagent.type}")
|
||||
# self.addItems(relevant_reagents)
|
||||
|
||||
# class SubmissionFormWidget(QWidget):
|
||||
|
||||
# def __init__(self, parent: QWidget, **kwargs) -> None:
|
||||
# super().__init__(parent)
|
||||
# # self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
# # "qt_scrollarea_vcontainer", "submit_btn"
|
||||
# # ]
|
||||
# self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx']
|
||||
# layout = QVBoxLayout()
|
||||
# for k, v in kwargs.items():
|
||||
# if k not in self.ignore:
|
||||
# add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type'])
|
||||
# if add_widget != None:
|
||||
# layout.addWidget(add_widget)
|
||||
# else:
|
||||
# setattr(self, k, v)
|
||||
|
||||
# self.setLayout(layout)
|
||||
|
||||
# def create_widget(self, key:str, value:dict, submission_type:str|None=None):
|
||||
# if key not in self.ignore:
|
||||
# return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
|
||||
# return None
|
||||
|
||||
# def clear_form(self):
|
||||
# for item in self.findChildren(QWidget):
|
||||
# item.setParent(None)
|
||||
|
||||
# def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
|
||||
# query = self.findChildren(QWidget)
|
||||
# if object_name != None:
|
||||
# query = [widget for widget in query if widget.objectName()==object_name]
|
||||
# return query
|
||||
|
||||
# def parse_form(self) -> PydSubmission:
|
||||
# logger.debug(f"Hello from form parser!")
|
||||
# info = {}
|
||||
# reagents = []
|
||||
# if hasattr(self, 'csv'):
|
||||
# info['csv'] = self.csv
|
||||
# for widget in self.findChildren(QWidget):
|
||||
# # logger.debug(f"Parsed widget of type {type(widget)}")
|
||||
# match widget:
|
||||
# case ReagentFormWidget():
|
||||
# reagent, _ = widget.parse_form()
|
||||
# if reagent != None:
|
||||
# reagents.append(reagent)
|
||||
# case self.InfoItem():
|
||||
# field, value = widget.parse_form()
|
||||
# if field != None:
|
||||
# info[field] = value
|
||||
# logger.debug(f"Info: {pformat(info)}")
|
||||
# logger.debug(f"Reagents: {pformat(reagents)}")
|
||||
# # app = self.parent().parent().parent().parent().parent().parent().parent().parent
|
||||
# submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
|
||||
# return submission
|
||||
|
||||
# class InfoItem(QWidget):
|
||||
|
||||
# def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> None:
|
||||
# super().__init__(parent)
|
||||
# layout = QVBoxLayout()
|
||||
# self.label = self.ParsedQLabel(key=key, value=value)
|
||||
# self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type['value'])
|
||||
# self.setObjectName(key)
|
||||
# try:
|
||||
# self.missing:bool = value['missing']
|
||||
# except (TypeError, KeyError):
|
||||
# self.missing:bool = True
|
||||
# if self.input != None:
|
||||
# layout.addWidget(self.label)
|
||||
# layout.addWidget(self.input)
|
||||
# layout.setContentsMargins(0,0,0,0)
|
||||
# self.setLayout(layout)
|
||||
# match self.input:
|
||||
# case QComboBox():
|
||||
# self.input.currentTextChanged.connect(self.update_missing)
|
||||
# case QDateEdit():
|
||||
# self.input.dateChanged.connect(self.update_missing)
|
||||
# case QLineEdit():
|
||||
# self.input.textChanged.connect(self.update_missing)
|
||||
|
||||
# def parse_form(self):
|
||||
# match self.input:
|
||||
# case QLineEdit():
|
||||
# value = self.input.text()
|
||||
# case QComboBox():
|
||||
# value = self.input.currentText()
|
||||
# case QDateEdit():
|
||||
# value = self.input.date().toPyDate()
|
||||
# case _:
|
||||
# return None, None
|
||||
# return self.input.objectName(), dict(value=value, missing=self.missing)
|
||||
|
||||
# def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
|
||||
# try:
|
||||
# value = value['value']
|
||||
# except (TypeError, KeyError):
|
||||
# pass
|
||||
# obj = parent.parent().parent()
|
||||
# logger.debug(f"Creating widget for: {key}")
|
||||
# match key:
|
||||
# case 'submitting_lab':
|
||||
# add_widget = QComboBox()
|
||||
# # lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
# labs = [item.__str__() for item in Organization.query()]
|
||||
# # try to set closest match to top of list
|
||||
# try:
|
||||
# labs = difflib.get_close_matches(value, labs, len(labs), 0)
|
||||
# except (TypeError, ValueError):
|
||||
# pass
|
||||
# # set combobox values to lookedup values
|
||||
# add_widget.addItems(labs)
|
||||
# case 'extraction_kit':
|
||||
# # if extraction kit not available, all other values fail
|
||||
# if not check_not_nan(value):
|
||||
# msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
|
||||
# msg.exec()
|
||||
# # create combobox to hold looked up kits
|
||||
# add_widget = QComboBox()
|
||||
# # lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
# logger.debug(f"Looking up kits used for {submission_type}")
|
||||
# uses = [item.__str__() for item in KitType.query(used_for=submission_type)]
|
||||
# obj.uses = uses
|
||||
# logger.debug(f"Kits received for {submission_type}: {uses}")
|
||||
# if check_not_nan(value):
|
||||
# logger.debug(f"The extraction kit in parser was: {value}")
|
||||
# uses.insert(0, uses.pop(uses.index(value)))
|
||||
# obj.ext_kit = value
|
||||
# else:
|
||||
# logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
||||
# obj.ext_kit = uses[0]
|
||||
# add_widget.addItems(uses)
|
||||
# # Run reagent scraper whenever extraction kit is changed.
|
||||
# # add_widget.currentTextChanged.connect(obj.scrape_reagents)
|
||||
# case 'submitted_date':
|
||||
# # uses base calendar
|
||||
# add_widget = QDateEdit(calendarPopup=True)
|
||||
# # sets submitted date based on date found in excel sheet
|
||||
# try:
|
||||
# add_widget.setDate(value)
|
||||
# # if not found, use today
|
||||
# except:
|
||||
# add_widget.setDate(date.today())
|
||||
# case 'submission_category':
|
||||
# add_widget = QComboBox()
|
||||
# cats = ['Diagnostic', "Surveillance", "Research"]
|
||||
# # cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
|
||||
# cats += [item.name for item in SubmissionType.query()]
|
||||
# try:
|
||||
# cats.insert(0, cats.pop(cats.index(value)))
|
||||
# except ValueError:
|
||||
# cats.insert(0, cats.pop(cats.index(submission_type)))
|
||||
# add_widget.addItems(cats)
|
||||
# case _:
|
||||
# # anything else gets added in as a line edit
|
||||
# add_widget = QLineEdit()
|
||||
# logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
|
||||
# add_widget.setText(str(value).replace("_", " "))
|
||||
# if add_widget != None:
|
||||
# add_widget.setObjectName(key)
|
||||
# add_widget.setParent(parent)
|
||||
|
||||
# return add_widget
|
||||
|
||||
# def update_missing(self):
|
||||
# self.missing = True
|
||||
# self.label.updated(self.objectName())
|
||||
|
||||
# class ParsedQLabel(QLabel):
|
||||
|
||||
# def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
|
||||
# super().__init__()
|
||||
# try:
|
||||
# check = not value['missing']
|
||||
# except:
|
||||
# check = True
|
||||
# if label_name != None:
|
||||
# self.setObjectName(label_name)
|
||||
# else:
|
||||
# self.setObjectName(f"{key}_label")
|
||||
# if title:
|
||||
# output = key.replace('_', ' ').title()
|
||||
# else:
|
||||
# output = key.replace('_', ' ')
|
||||
# if check:
|
||||
# self.setText(f"Parsed {output}")
|
||||
# else:
|
||||
# self.setText(f"MISSING {output}")
|
||||
|
||||
# def updated(self, key:str, title:bool=True):
|
||||
# if title:
|
||||
# output = key.replace('_', ' ').title()
|
||||
# else:
|
||||
# output = key.replace('_', ' ')
|
||||
# self.setText(f"UPDATED {output}")
|
||||
|
||||
@@ -17,15 +17,19 @@ from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
||||
from backend.db.functions import submissions_to_df
|
||||
from backend.db.models import BasicSubmission
|
||||
from backend.excel import make_hitpicks
|
||||
from tools import check_if_app, Settings
|
||||
from backend.excel import make_hitpicks, make_report_html, make_report_xlsx
|
||||
from tools import check_if_app, Settings, Report, Result
|
||||
from tools import jinja_template_loading
|
||||
from xhtml2pdf import pisa
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from .pop_ups import QuestionAsker, AlertPop
|
||||
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
|
||||
from .functions import select_save_file, select_open_file
|
||||
from .misc import ReportDatePicker
|
||||
import pandas as pd
|
||||
from getpass import getuser
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -79,15 +83,17 @@ class SubmissionsSheet(QTableView):
|
||||
"""
|
||||
presents submission summary to user in tab1
|
||||
"""
|
||||
def __init__(self, ctx:Settings) -> None:
|
||||
def __init__(self, parent) -> None:
|
||||
"""
|
||||
initialize
|
||||
|
||||
Args:
|
||||
ctx (dict): settings passed from gui
|
||||
"""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
super().__init__(parent)
|
||||
self.app = self.parent()
|
||||
# self.ctx = ctx
|
||||
self.report = Report()
|
||||
self.setData()
|
||||
self.resizeColumnsToContents()
|
||||
self.resizeRowsToContents()
|
||||
@@ -110,14 +116,14 @@ class SubmissionsSheet(QTableView):
|
||||
proxyModel.setSourceModel(pandasModel(self.data))
|
||||
self.setModel(proxyModel)
|
||||
|
||||
|
||||
def show_details(self) -> None:
|
||||
"""
|
||||
creates detailed data to show in seperate window
|
||||
"""
|
||||
logger.debug(f"Sheet.app: {self.app}")
|
||||
index = (self.selectionModel().currentIndex())
|
||||
value = index.sibling(index.row(),0).data()
|
||||
dlg = SubmissionDetails(ctx=self.ctx, id=value)
|
||||
dlg = SubmissionDetails(parent=self, id=value)
|
||||
if dlg.exec():
|
||||
pass
|
||||
|
||||
@@ -245,15 +251,210 @@ class SubmissionsSheet(QTableView):
|
||||
image.show()
|
||||
except Exception as e:
|
||||
logger.error(f"Could not show image: {e}.")
|
||||
|
||||
|
||||
def link_extractions(self):
|
||||
self.link_extractions_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
|
||||
def link_extractions_function(self):
|
||||
"""
|
||||
Link extractions from runlogs to imported submissions
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
fname = select_open_file(self, file_extension="csv")
|
||||
with open(fname.__str__(), 'r') as f:
|
||||
# split csv on commas
|
||||
runs = [col.strip().split(",") for col in f.readlines()]
|
||||
count = 0
|
||||
for run in runs:
|
||||
new_run = dict(
|
||||
start_time=run[0].strip(),
|
||||
rsl_plate_num=run[1].strip(),
|
||||
sample_count=run[2].strip(),
|
||||
status=run[3].strip(),
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# elution columns are item 6 in the comma split list to the end
|
||||
for ii in range(6, len(run)):
|
||||
new_run[f"column{str(ii-5)}_vol"] = run[ii]
|
||||
# Lookup imported submissions
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
||||
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
||||
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
|
||||
# If no such submission exists, move onto the next run
|
||||
if sub == None:
|
||||
continue
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
count += 1
|
||||
except AttributeError:
|
||||
continue
|
||||
if sub.extraction_info != None:
|
||||
existing = json.loads(sub.extraction_info)
|
||||
else:
|
||||
existing = None
|
||||
# Check if the new info already exists in the imported submission
|
||||
try:
|
||||
if json.dumps(new_run) in sub.extraction_info:
|
||||
logger.debug(f"Looks like we already have that info.")
|
||||
continue
|
||||
except TypeError:
|
||||
pass
|
||||
# Update or create the extraction info
|
||||
if existing != None:
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}")
|
||||
existing.append(new_run)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.extraction_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.extraction_info = json.dumps([new_run])
|
||||
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.extraction_info}")
|
||||
else:
|
||||
sub.extraction_info = json.dumps([new_run])
|
||||
sub.save()
|
||||
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
|
||||
|
||||
def link_pcr(self):
|
||||
self.link_pcr_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
|
||||
def link_pcr_function(self):
|
||||
"""
|
||||
Link PCR data from run logs to an imported submission
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
fname = select_open_file(self, file_extension="csv")
|
||||
with open(fname.__str__(), 'r') as f:
|
||||
# split csv rows on comma
|
||||
runs = [col.strip().split(",") for col in f.readlines()]
|
||||
count = 0
|
||||
for run in runs:
|
||||
new_run = dict(
|
||||
start_time=run[0].strip(),
|
||||
rsl_plate_num=run[1].strip(),
|
||||
biomek_status=run[2].strip(),
|
||||
quant_status=run[3].strip(),
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# lookup imported submission
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num'])
|
||||
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num'])
|
||||
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
|
||||
# if imported submission doesn't exist move on to next run
|
||||
if sub == None:
|
||||
continue
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
continue
|
||||
# check if pcr_info already exists
|
||||
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
|
||||
existing = json.loads(sub.pcr_info)
|
||||
else:
|
||||
existing = None
|
||||
# check if this entry already exists in imported submission
|
||||
try:
|
||||
if json.dumps(new_run) in sub.pcr_info:
|
||||
logger.debug(f"Looks like we already have that info.")
|
||||
continue
|
||||
else:
|
||||
count += 1
|
||||
except TypeError:
|
||||
logger.error(f"No json to dump")
|
||||
if existing != None:
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}")
|
||||
existing.append(new_run)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.pcr_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.pcr_info = json.dumps([new_run])
|
||||
logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.pcr_info}")
|
||||
else:
|
||||
sub.pcr_info = json.dumps([new_run])
|
||||
sub.save()
|
||||
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
|
||||
|
||||
def generate_report(self):
|
||||
self.generate_report_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
|
||||
def generate_report_function(self):
|
||||
"""
|
||||
Generate a summary of activities for a time period
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
# ask for date ranges
|
||||
dlg = ReportDatePicker()
|
||||
if dlg.exec():
|
||||
info = dlg.parse_form()
|
||||
logger.debug(f"Report info: {info}")
|
||||
# find submissions based on date range
|
||||
subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date'])
|
||||
# convert each object to dict
|
||||
records = [item.report_dict() for item in subs]
|
||||
# make dataframe from record dictionaries
|
||||
detailed_df, summary_df = make_report_xlsx(records=records)
|
||||
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
||||
# get save location of report
|
||||
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf", extension="pdf")
|
||||
with open(fname, "w+b") as f:
|
||||
pisa.CreatePDF(html, dest=f)
|
||||
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
||||
summary_df.to_excel(writer, sheet_name="Report")
|
||||
detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
||||
worksheet = writer.sheets['Report']
|
||||
for idx, col in enumerate(summary_df): # loop through all columns
|
||||
series = summary_df[col]
|
||||
max_len = max((
|
||||
series.astype(str).map(len).max(), # len of largest item
|
||||
len(str(series.name)) # len of column name/header
|
||||
)) + 20 # adding a little extra space
|
||||
try:
|
||||
worksheet.column_dimensions[get_column_letter(idx)].width = max_len
|
||||
except ValueError:
|
||||
pass
|
||||
for cell in worksheet['D']:
|
||||
if cell.row > 1:
|
||||
cell.style = 'Currency'
|
||||
writer.close()
|
||||
self.report.add_result(report)
|
||||
|
||||
class SubmissionDetails(QDialog):
|
||||
"""
|
||||
a window showing text details of submission
|
||||
"""
|
||||
def __init__(self, ctx:dict, id:int) -> None:
|
||||
def __init__(self, parent, id:int) -> None:
|
||||
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
super().__init__(parent)
|
||||
# self.ctx = ctx
|
||||
self.setWindowTitle("Submission Details")
|
||||
# create scrollable interior
|
||||
interior = QScrollArea()
|
||||
@@ -300,14 +501,15 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
Renders submission to html, then creates and saves .pdf file to user selected file.
|
||||
"""
|
||||
try:
|
||||
home_dir = Path(self.ctx.directory_path).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
|
||||
except FileNotFoundError:
|
||||
home_dir = Path.home().resolve().__str__()
|
||||
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
|
||||
if fname.__str__() == ".":
|
||||
logger.debug("Saving pdf was cancelled.")
|
||||
return
|
||||
# try:
|
||||
# home_dir = Path(self.ctx.directory_path).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
|
||||
# except FileNotFoundError:
|
||||
# home_dir = Path.home().resolve().__str__()
|
||||
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
|
||||
# if fname.__str__() == ".":
|
||||
# logger.debug("Saving pdf was cancelled.")
|
||||
# return
|
||||
fname = select_save_file(obj=self, default_name=self.base_dict['Plate Number'], extension="pdf")
|
||||
del self.base_dict['platemap']
|
||||
export_map = make_plate_map(self.plate_dicto)
|
||||
image_io = BytesIO()
|
||||
747
src/submissions/frontend/widgets/submission_widget.py
Normal file
747
src/submissions/frontend/widgets/submission_widget.py
Normal file
@@ -0,0 +1,747 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout,
|
||||
QComboBox, QDateEdit, QLineEdit, QLabel
|
||||
)
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
from pathlib import Path
|
||||
from . import select_open_file, select_save_file
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from tools import Report, Result, check_not_nan
|
||||
from backend.excel.parser import SheetParser, PCRParser
|
||||
from backend.validators import PydSubmission, PydReagent
|
||||
from backend.db import (
|
||||
check_kit_integrity, update_last_used, KitType, Organization, SubmissionType, Reagent,
|
||||
ReagentType, KitTypeReagentTypeAssociation, BasicSubmission, update_subsampassoc_with_pcr
|
||||
)
|
||||
from pprint import pformat
|
||||
from .pop_ups import QuestionAsker, AlertPop
|
||||
# from .misc import ReagentFormWidget
|
||||
from typing import List, Tuple
|
||||
import difflib
|
||||
from datetime import date
|
||||
import inspect
|
||||
import json
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class SubmissionFormContainer(QWidget):
|
||||
|
||||
import_drag = pyqtSignal(Path)
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
logger.debug(f"Setting form widget...")
|
||||
super().__init__(parent)
|
||||
self.app = self.parent().parent#().parent().parent().parent().parent().parent
|
||||
self.report = Report()
|
||||
# self.parent = parent
|
||||
self.setAcceptDrops(True)
|
||||
self.import_drag.connect(self.importSubmission)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if event.mimeData().hasUrls():
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def dropEvent(self, event):
|
||||
fname = Path([u.toLocalFile() for u in event.mimeData().urls()][0])
|
||||
|
||||
logger.debug(f"App: {self.app}")
|
||||
self.app.last_dir = fname.parent
|
||||
self.import_drag.emit(fname)
|
||||
|
||||
def importSubmission(self, fname:Path|None=None):
|
||||
"""
|
||||
import submission from excel sheet into form
|
||||
"""
|
||||
# from .main_window_functions import import_submission_function
|
||||
self.app.raise_()
|
||||
self.app.activateWindow()
|
||||
self.import_submission_function(fname)
|
||||
logger.debug(f"Result from result reporter: {self.report.results}")
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
|
||||
def scrape_reagents(self, *args, **kwargs):
|
||||
# from .main_window_functions import scrape_reagents
|
||||
# logger.debug(f"Args: {args}")
|
||||
# logger.debug(F"kwargs: {kwargs}")
|
||||
print(f"\n\n{inspect.stack()[1].function}\n\n")
|
||||
self.scrape_reagents_function(args[0])
|
||||
self.kit_integrity_completion()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
match inspect.stack()[1].function:
|
||||
case "import_submission_function":
|
||||
pass
|
||||
case _:
|
||||
self.app.result_reporter()
|
||||
|
||||
# def kit_reload_function(self):
|
||||
# """
|
||||
# Reload the fields in the form
|
||||
|
||||
# Args:
|
||||
# obj (QMainWindow): original app window
|
||||
|
||||
# Returns:
|
||||
# Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
# """
|
||||
# report = Report()
|
||||
# # for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget):
|
||||
# logger.debug(f"Attempting to clear {obj.form.find_widgets()}")
|
||||
# for item in self.form.find_widgets():
|
||||
# if isinstance(item, ReagentFormWidget):
|
||||
# item.setParent(None)
|
||||
# self.kit_integrity_completion_function()
|
||||
# self.report.add_result(report)
|
||||
|
||||
|
||||
|
||||
def kit_integrity_completion(self):
|
||||
"""
|
||||
Performs check of imported reagents
|
||||
NOTE: this will not change self.reagents which should be fine
|
||||
since it's only used when looking up
|
||||
"""
|
||||
# from .main_window_functions import kit_integrity_completion_function
|
||||
self.kit_integrity_completion_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
match inspect.stack()[1].function:
|
||||
case "import_submission_function":
|
||||
pass
|
||||
case _:
|
||||
self.app.result_reporter()
|
||||
|
||||
def submit_new_sample(self):
|
||||
"""
|
||||
Attempt to add sample to database when 'submit' button clicked
|
||||
"""
|
||||
# from .main_window_functions import submit_new_sample_function
|
||||
self.submit_new_sample_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
|
||||
def export_csv(self, fname:Path|None=None):
|
||||
# from .main_window_functions import export_csv_function
|
||||
self.export_csv_function(fname)
|
||||
|
||||
def import_submission_function(self, fname:Path|None=None):
|
||||
"""
|
||||
Import a new submission to the app window
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict|None]: Collection of new main app window and result dict
|
||||
"""
|
||||
logger.debug(f"\n\nStarting Import...\n\n")
|
||||
report = Report()
|
||||
# logger.debug(obj.ctx)
|
||||
# initialize samples
|
||||
try:
|
||||
self.form.setParent(None)
|
||||
except AttributeError:
|
||||
pass
|
||||
self.samples = []
|
||||
self.missing_info = []
|
||||
# set file dialog
|
||||
if isinstance(fname, bool) or fname == None:
|
||||
fname = select_open_file(self, file_extension="xlsx")
|
||||
logger.debug(f"Attempting to parse file: {fname}")
|
||||
if not fname.exists():
|
||||
# result = dict(message=f"File {fname.__str__()} not found.", status="critical")
|
||||
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
|
||||
self.report.add_result(report)
|
||||
return
|
||||
# create sheetparser using excel sheet and context from gui
|
||||
try:
|
||||
self.prsr = SheetParser(ctx=self.ctx, filepath=fname)
|
||||
except PermissionError:
|
||||
logger.error(f"Couldn't get permission to access file: {fname}")
|
||||
return
|
||||
except AttributeError:
|
||||
self.prsr = SheetParser(ctx=self.app.ctx, filepath=fname)
|
||||
try:
|
||||
logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}")
|
||||
self.pyd = self.prsr.to_pydantic()
|
||||
logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n")
|
||||
except Exception as e:
|
||||
report.add_result(Result(msg=f"Problem creating pydantic model:\n\n{e}", status="Critical"))
|
||||
self.report.add_result(report)
|
||||
return
|
||||
self.form = self.pyd.toForm(parent=self)
|
||||
self.layout().addWidget(self.form)
|
||||
kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input
|
||||
logger.debug(f"Kitwidget {kit_widget}")
|
||||
self.scrape_reagents(kit_widget.currentText())
|
||||
kit_widget.currentTextChanged.connect(self.scrape_reagents)
|
||||
# compare obj.reagents with expected reagents in kit
|
||||
if self.prsr.sample_result != None:
|
||||
report.add_result(msg=self.prsr.sample_result, status="Warning")
|
||||
self.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {self.report.results}")
|
||||
logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
||||
|
||||
def scrape_reagents_function(self, extraction_kit:str):
|
||||
"""
|
||||
Extracted scrape reagents function that will run when
|
||||
form 'extraction_kit' widget is updated.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): updated main application
|
||||
extraction_kit (str): name of extraction kit (in 'extraction_kit' widget)
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Updated application and result
|
||||
"""
|
||||
report = Report()
|
||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||
# obj.reagents = []
|
||||
# obj.missing_reagents = []
|
||||
# Remove previous reagent widgets
|
||||
try:
|
||||
old_reagents = self.form.find_widgets()
|
||||
except AttributeError:
|
||||
logger.error(f"Couldn't find old reagents.")
|
||||
old_reagents = []
|
||||
# logger.debug(f"\n\nAttempting to clear: {old_reagents}\n\n")
|
||||
for reagent in old_reagents:
|
||||
if isinstance(reagent, ReagentFormWidget) or isinstance(reagent, QPushButton):
|
||||
reagent.setParent(None)
|
||||
# reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
|
||||
# logger.debug(f"Got reagents: {reagents}")
|
||||
# for reagent in obj.prsr.sub['reagents']:
|
||||
# # create label
|
||||
# if reagent.parsed:
|
||||
# obj.reagents.append(reagent)
|
||||
# else:
|
||||
# obj.missing_reagents.append(reagent)
|
||||
self.form.reagents = self.prsr.sub['reagents']
|
||||
# logger.debug(f"Imported reagents: {obj.reagents}")
|
||||
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
||||
self.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {self.report.results}")
|
||||
|
||||
def kit_integrity_completion_function(self):
|
||||
"""
|
||||
Compare kit contents to parsed contents
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): The original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
missing_reagents = []
|
||||
# logger.debug(inspect.currentframe().f_back.f_code.co_name)
|
||||
# find the widget that contains kit info
|
||||
kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input
|
||||
logger.debug(f"Kit selector: {kit_widget}")
|
||||
# get current kit being used
|
||||
self.ext_kit = kit_widget.currentText()
|
||||
# for reagent in obj.pyd.reagents:
|
||||
for reagent in self.form.reagents:
|
||||
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
|
||||
# add_widget.setParent(sub_form_container.form)
|
||||
self.form.layout().addWidget(add_widget)
|
||||
if reagent.missing:
|
||||
missing_reagents.append(reagent)
|
||||
logger.debug(f"Checking integrity of {self.ext_kit}")
|
||||
# TODO: put check_kit_integrity here instead of what's here?
|
||||
# see if there are any missing reagents
|
||||
if len(missing_reagents) > 0:
|
||||
result = Result(msg=f"""The submission you are importing is missing some reagents expected by the kit.\n\n
|
||||
It looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\n
|
||||
Alternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.
|
||||
\n\nPlease make sure you check the lots carefully!""".replace(" ", ""), status="Warning")
|
||||
report.add_result(result)
|
||||
if hasattr(self.pyd, "csv"):
|
||||
export_csv_btn = QPushButton("Export CSV")
|
||||
export_csv_btn.setObjectName("export_csv_btn")
|
||||
self.form.layout().addWidget(export_csv_btn)
|
||||
export_csv_btn.clicked.connect(self.export_csv)
|
||||
submit_btn = QPushButton("Submit")
|
||||
submit_btn.setObjectName("submit_btn")
|
||||
self.form.layout().addWidget(submit_btn)
|
||||
submit_btn.clicked.connect(self.submit_new_sample)
|
||||
self.report.add_result(report)
|
||||
logger.debug(f"Outgoing report: {self.report.results}")
|
||||
|
||||
def submit_new_sample_function(self) -> QWidget:
|
||||
"""
|
||||
Parse forms and add sample to the database.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
logger.debug(f"\n\nBeginning Submission\n\n")
|
||||
report = Report()
|
||||
self.pyd: PydSubmission = self.form.parse_form()
|
||||
logger.debug(f"Submission: {pformat(self.pyd)}")
|
||||
logger.debug("Checking kit integrity...")
|
||||
result = check_kit_integrity(sub=self.pyd)
|
||||
report.add_result(result)
|
||||
if len(result.results) > 0:
|
||||
self.report.add_result(report)
|
||||
return
|
||||
base_submission, result = self.pyd.toSQL()
|
||||
# check output message for issues
|
||||
match result.code:
|
||||
# code 0: everything is fine.
|
||||
case 0:
|
||||
self.report.add_result(None)
|
||||
# code 1: ask for overwrite
|
||||
case 1:
|
||||
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result['message'])
|
||||
if dlg.exec():
|
||||
# Do not add duplicate reagents.
|
||||
# base_submission.reagents = []
|
||||
result = None
|
||||
else:
|
||||
self.app.ctx.database_session.rollback()
|
||||
self.report.add_result(Result(msg="Overwrite cancelled", status="Information"))
|
||||
return
|
||||
# code 2: No RSL plate number given
|
||||
case 2:
|
||||
self.report.add_result(result)
|
||||
return
|
||||
case _:
|
||||
pass
|
||||
# add reagents to submission object
|
||||
for reagent in base_submission.reagents:
|
||||
update_last_used(reagent=reagent, kit=base_submission.extraction_kit)
|
||||
logger.debug(f"Here is the final submission: {pformat(base_submission.__dict__)}")
|
||||
logger.debug(f"Parsed reagents: {pformat(base_submission.reagents)}")
|
||||
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
|
||||
base_submission.save()
|
||||
# update summary sheet
|
||||
self.app.table_widget.sub_wid.setData()
|
||||
# reset form
|
||||
self.form.setParent(None)
|
||||
logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
|
||||
wkb = self.pyd.autofill_excel()
|
||||
if wkb != None:
|
||||
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="xlsx")
|
||||
try:
|
||||
wkb.save(filename=fname.__str__())
|
||||
except PermissionError:
|
||||
logger.error("Hit a permission error when saving workbook. Cancelled?")
|
||||
if hasattr(self.pyd, 'csv'):
|
||||
dlg = QuestionAsker("Export CSV?", "Would you like to export the csv file?")
|
||||
if dlg.exec():
|
||||
fname = select_save_file(self, f"{self.pyd.construct_filename()}.csv", extension="csv")
|
||||
try:
|
||||
self.pyd.csv.to_csv(fname.__str__(), index=False)
|
||||
except PermissionError:
|
||||
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
self.report.add_result(report)
|
||||
|
||||
def export_csv_function(self, fname:Path|None=None):
|
||||
if isinstance(fname, bool) or fname == None:
|
||||
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
|
||||
try:
|
||||
self.pyd.csv.to_csv(fname.__str__(), index=False)
|
||||
except PermissionError:
|
||||
logger.debug(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
|
||||
def import_pcr_results(self):
|
||||
self.import_pcr_results_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
self.app.result_reporter()
|
||||
|
||||
def import_pcr_results_function(self):
|
||||
"""
|
||||
Import Quant-studio PCR data to an imported submission
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
|
||||
"""
|
||||
report = Report()
|
||||
fname = select_open_file(self, file_extension="xlsx")
|
||||
parser = PCRParser(filepath=fname)
|
||||
logger.debug(f"Attempting lookup for {parser.plate_num}")
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
|
||||
sub = BasicSubmission.query(rsl_number=parser.plate_num)
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
# If no plate is found, may be because this is a repeat. Lop off the '-1' or '-2' and repeat
|
||||
logger.error(f"Submission of number {parser.plate_num} not found. Attempting rescue of plate repeat.")
|
||||
parser.plate_num = "-".join(parser.plate_num.split("-")[:-1])
|
||||
# sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=parser.plate_num)
|
||||
# sub = lookup_submissions(ctx=obj.ctx, rsl_number=parser.plate_num)
|
||||
sub = BasicSubmission.query(rsl_number=parser.plate_num)
|
||||
try:
|
||||
logger.debug(f"Found submission: {sub.rsl_plate_num}")
|
||||
except AttributeError:
|
||||
logger.error(f"Rescue of {parser.plate_num} failed.")
|
||||
# return obj, dict(message="Couldn't find a submission with that RSL number.", status="warning")
|
||||
self.report.add_result(Result(msg="Couldn't find a submission with that RSL number.", status="Warning"))
|
||||
return
|
||||
# Check if PCR info already exists
|
||||
if hasattr(sub, 'pcr_info') and sub.pcr_info != None:
|
||||
existing = json.loads(sub.pcr_info)
|
||||
else:
|
||||
existing = None
|
||||
if existing != None:
|
||||
# update pcr_info
|
||||
try:
|
||||
logger.debug(f"Updating {type(existing)}: {existing} with {type(parser.pcr)}: {parser.pcr}")
|
||||
if json.dumps(parser.pcr) not in sub.pcr_info:
|
||||
existing.append(parser.pcr)
|
||||
logger.debug(f"Setting: {existing}")
|
||||
sub.pcr_info = json.dumps(existing)
|
||||
except TypeError:
|
||||
logger.error(f"Error updating!")
|
||||
sub.pcr_info = json.dumps([parser.pcr])
|
||||
logger.debug(f"Final pcr info for {sub.rsl_plate_num}: {sub.pcr_info}")
|
||||
else:
|
||||
sub.pcr_info = json.dumps([parser.pcr])
|
||||
# obj.ctx.database_session.add(sub)
|
||||
logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}")
|
||||
logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}")
|
||||
sub.save()
|
||||
logger.debug(f"Got {len(parser.samples)} samples to update!")
|
||||
logger.debug(f"Parser samples: {parser.samples}")
|
||||
for sample in sub.samples:
|
||||
logger.debug(f"Running update on: {sample}")
|
||||
try:
|
||||
sample_dict = [item for item in parser.samples if item['sample']==sample.rsl_number][0]
|
||||
except IndexError:
|
||||
continue
|
||||
update_subsampassoc_with_pcr(submission=sub, sample=sample, input_dict=sample_dict)
|
||||
self.report.add_result(Result(msg=f"We added PCR info to {sub.rsl_plate_num}.", status='Information'))
|
||||
# return obj, result
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, **kwargs) -> None:
|
||||
super().__init__(parent)
|
||||
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
# "qt_scrollarea_vcontainer", "submit_btn"
|
||||
# ]
|
||||
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx']
|
||||
layout = QVBoxLayout()
|
||||
for k, v in kwargs.items():
|
||||
if k not in self.ignore:
|
||||
add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type'])
|
||||
if add_widget != None:
|
||||
layout.addWidget(add_widget)
|
||||
else:
|
||||
setattr(self, k, v)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_widget(self, key:str, value:dict, submission_type:str|None=None):
|
||||
if key not in self.ignore:
|
||||
return self.InfoItem(self, key=key, value=value, submission_type=submission_type)
|
||||
return None
|
||||
|
||||
def clear_form(self):
|
||||
for item in self.findChildren(QWidget):
|
||||
item.setParent(None)
|
||||
|
||||
def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
|
||||
query = self.findChildren(QWidget)
|
||||
if object_name != None:
|
||||
query = [widget for widget in query if widget.objectName()==object_name]
|
||||
return query
|
||||
|
||||
def parse_form(self) -> PydSubmission:
|
||||
logger.debug(f"Hello from form parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
if hasattr(self, 'csv'):
|
||||
info['csv'] = self.csv
|
||||
for widget in self.findChildren(QWidget):
|
||||
# logger.debug(f"Parsed widget of type {type(widget)}")
|
||||
match widget:
|
||||
case ReagentFormWidget():
|
||||
reagent, _ = widget.parse_form()
|
||||
if reagent != None:
|
||||
reagents.append(reagent)
|
||||
case self.InfoItem():
|
||||
field, value = widget.parse_form()
|
||||
if field != None:
|
||||
info[field] = value
|
||||
logger.debug(f"Info: {pformat(info)}")
|
||||
logger.debug(f"Reagents: {pformat(reagents)}")
|
||||
# app = self.parent().parent().parent().parent().parent().parent().parent().parent
|
||||
submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
|
||||
return submission
|
||||
|
||||
class InfoItem(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> None:
|
||||
super().__init__(parent)
|
||||
layout = QVBoxLayout()
|
||||
self.label = self.ParsedQLabel(key=key, value=value)
|
||||
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type['value'])
|
||||
self.setObjectName(key)
|
||||
try:
|
||||
self.missing:bool = value['missing']
|
||||
except (TypeError, KeyError):
|
||||
self.missing:bool = True
|
||||
if self.input != None:
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.input)
|
||||
layout.setContentsMargins(0,0,0,0)
|
||||
self.setLayout(layout)
|
||||
match self.input:
|
||||
case QComboBox():
|
||||
self.input.currentTextChanged.connect(self.update_missing)
|
||||
case QDateEdit():
|
||||
self.input.dateChanged.connect(self.update_missing)
|
||||
case QLineEdit():
|
||||
self.input.textChanged.connect(self.update_missing)
|
||||
|
||||
def parse_form(self):
|
||||
match self.input:
|
||||
case QLineEdit():
|
||||
value = self.input.text()
|
||||
case QComboBox():
|
||||
value = self.input.currentText()
|
||||
case QDateEdit():
|
||||
value = self.input.date().toPyDate()
|
||||
case _:
|
||||
return None, None
|
||||
return self.input.objectName(), dict(value=value, missing=self.missing)
|
||||
|
||||
def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
|
||||
try:
|
||||
value = value['value']
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
obj = parent.parent().parent()
|
||||
logger.debug(f"Creating widget for: {key}")
|
||||
match key:
|
||||
case 'submitting_lab':
|
||||
add_widget = QComboBox()
|
||||
# lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
labs = [item.__str__() for item in Organization.query()]
|
||||
# try to set closest match to top of list
|
||||
try:
|
||||
labs = difflib.get_close_matches(value, labs, len(labs), 0)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
# set combobox values to lookedup values
|
||||
add_widget.addItems(labs)
|
||||
case 'extraction_kit':
|
||||
# if extraction kit not available, all other values fail
|
||||
if not check_not_nan(value):
|
||||
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
|
||||
msg.exec()
|
||||
# create combobox to hold looked up kits
|
||||
add_widget = QComboBox()
|
||||
# lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
logger.debug(f"Looking up kits used for {submission_type}")
|
||||
uses = [item.__str__() for item in KitType.query(used_for=submission_type)]
|
||||
obj.uses = uses
|
||||
logger.debug(f"Kits received for {submission_type}: {uses}")
|
||||
if check_not_nan(value):
|
||||
logger.debug(f"The extraction kit in parser was: {value}")
|
||||
uses.insert(0, uses.pop(uses.index(value)))
|
||||
obj.ext_kit = value
|
||||
else:
|
||||
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
||||
obj.ext_kit = uses[0]
|
||||
add_widget.addItems(uses)
|
||||
# Run reagent scraper whenever extraction kit is changed.
|
||||
# add_widget.currentTextChanged.connect(obj.scrape_reagents)
|
||||
case 'submitted_date':
|
||||
# uses base calendar
|
||||
add_widget = QDateEdit(calendarPopup=True)
|
||||
# sets submitted date based on date found in excel sheet
|
||||
try:
|
||||
add_widget.setDate(value)
|
||||
# if not found, use today
|
||||
except:
|
||||
add_widget.setDate(date.today())
|
||||
case 'submission_category':
|
||||
add_widget = QComboBox()
|
||||
cats = ['Diagnostic', "Surveillance", "Research"]
|
||||
# cats += [item.name for item in lookup_submission_type(ctx=obj.ctx)]
|
||||
cats += [item.name for item in SubmissionType.query()]
|
||||
try:
|
||||
cats.insert(0, cats.pop(cats.index(value)))
|
||||
except ValueError:
|
||||
cats.insert(0, cats.pop(cats.index(submission_type)))
|
||||
add_widget.addItems(cats)
|
||||
case _:
|
||||
# anything else gets added in as a line edit
|
||||
add_widget = QLineEdit()
|
||||
logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
|
||||
add_widget.setText(str(value).replace("_", " "))
|
||||
if add_widget != None:
|
||||
add_widget.setObjectName(key)
|
||||
add_widget.setParent(parent)
|
||||
|
||||
return add_widget
|
||||
|
||||
def update_missing(self):
|
||||
self.missing = True
|
||||
self.label.updated(self.objectName())
|
||||
|
||||
class ParsedQLabel(QLabel):
|
||||
|
||||
def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
|
||||
super().__init__()
|
||||
try:
|
||||
check = not value['missing']
|
||||
except:
|
||||
check = True
|
||||
if label_name != None:
|
||||
self.setObjectName(label_name)
|
||||
else:
|
||||
self.setObjectName(f"{key}_label")
|
||||
if title:
|
||||
output = key.replace('_', ' ').title()
|
||||
else:
|
||||
output = key.replace('_', ' ')
|
||||
if check:
|
||||
self.setText(f"Parsed {output}")
|
||||
else:
|
||||
self.setText(f"MISSING {output}")
|
||||
|
||||
def updated(self, key:str, title:bool=True):
|
||||
if title:
|
||||
output = key.replace('_', ' ').title()
|
||||
else:
|
||||
output = key.replace('_', ' ')
|
||||
self.setText(f"UPDATED {output}")
|
||||
|
||||
class ReagentFormWidget(QWidget):
|
||||
|
||||
def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
|
||||
super().__init__(parent)
|
||||
# self.setParent(parent)
|
||||
self.reagent = reagent
|
||||
self.extraction_kit = extraction_kit
|
||||
# self.ctx = reagent.ctx
|
||||
layout = QVBoxLayout()
|
||||
self.label = self.ReagentParsedLabel(reagent=reagent)
|
||||
layout.addWidget(self.label)
|
||||
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
||||
layout.addWidget(self.lot)
|
||||
# Remove spacing between reagents
|
||||
layout.setContentsMargins(0,0,0,0)
|
||||
self.setLayout(layout)
|
||||
self.setObjectName(reagent.name)
|
||||
self.missing = reagent.missing
|
||||
# If changed set self.missing to True and update self.label
|
||||
self.lot.currentTextChanged.connect(self.updated)
|
||||
|
||||
def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||
lot = self.lot.currentText()
|
||||
# wanted_reagent = lookup_reagents(ctx=self.ctx, lot_number=lot, reagent_type=self.reagent.type)
|
||||
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
||||
# if reagent doesn't exist in database, off to add it (uses App.add_reagent)
|
||||
if wanted_reagent == None:
|
||||
dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
|
||||
if dlg.exec():
|
||||
wanted_reagent = self.parent().parent().parent().parent().parent().parent().parent().parent().parent.add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
|
||||
return wanted_reagent, None
|
||||
else:
|
||||
# In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||
logger.debug("Will not add reagent.")
|
||||
return None, Result(msg="Failed integrity check", status="Critical")
|
||||
else:
|
||||
# Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
||||
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
||||
# rt = lookup_reagent_types(ctx=self.ctx, name=self.reagent.type)
|
||||
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
rt = ReagentType.query(name=self.reagent.type)
|
||||
if rt == None:
|
||||
# rt = lookup_reagent_types(ctx=self.ctx, kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, parsed=not self.missing), None
|
||||
|
||||
def updated(self):
|
||||
self.missing = True
|
||||
self.label.updated(self.reagent.type)
|
||||
|
||||
class ReagentParsedLabel(QLabel):
|
||||
|
||||
def __init__(self, reagent:PydReagent):
|
||||
super().__init__()
|
||||
try:
|
||||
check = not reagent.missing
|
||||
except:
|
||||
check = False
|
||||
self.setObjectName(f"{reagent.type}_label")
|
||||
if check:
|
||||
self.setText(f"Parsed {reagent.type}")
|
||||
else:
|
||||
self.setText(f"MISSING {reagent.type}")
|
||||
|
||||
def updated(self, reagent_type:str):
|
||||
self.setText(f"UPDATED {reagent_type}")
|
||||
|
||||
class ReagentLot(QComboBox):
|
||||
|
||||
def __init__(self, reagent, extraction_kit:str) -> None:
|
||||
super().__init__()
|
||||
# self.ctx = reagent.ctx
|
||||
self.setEditable(True)
|
||||
# if reagent.parsed:
|
||||
# pass
|
||||
logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
|
||||
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
|
||||
# lookup = lookup_reagents(ctx=self.ctx, reagent_type=reagent.type)
|
||||
lookup = Reagent.query(reagent_type=reagent.type)
|
||||
relevant_reagents = [item.__str__() for item in lookup]
|
||||
output_reg = []
|
||||
for rel_reagent in relevant_reagents:
|
||||
# extract strings from any sets.
|
||||
if isinstance(rel_reagent, set):
|
||||
for thing in rel_reagent:
|
||||
output_reg.append(thing)
|
||||
elif isinstance(rel_reagent, str):
|
||||
output_reg.append(rel_reagent)
|
||||
relevant_reagents = output_reg
|
||||
# if reagent in sheet is not found insert it into the front of relevant reagents so it shows
|
||||
logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}")
|
||||
if str(reagent.lot) not in relevant_reagents:
|
||||
if check_not_nan(reagent.lot):
|
||||
relevant_reagents.insert(0, str(reagent.lot))
|
||||
else:
|
||||
# TODO: look up the last used reagent of this type in the database
|
||||
# looked_up_rt = lookup_reagenttype_kittype_association(ctx=self.ctx, reagent_type=reagent.type, kit_type=extraction_kit)
|
||||
looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
|
||||
try:
|
||||
# looked_up_reg = lookup_reagents(ctx=self.ctx, lot_number=looked_up_rt.last_used)
|
||||
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
||||
except AttributeError:
|
||||
looked_up_reg = None
|
||||
logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}")
|
||||
if looked_up_reg != None:
|
||||
relevant_reagents.remove(str(looked_up_reg.lot))
|
||||
relevant_reagents.insert(0, str(looked_up_reg.lot))
|
||||
else:
|
||||
if len(relevant_reagents) > 1:
|
||||
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.")
|
||||
idx = relevant_reagents.index(str(reagent.lot))
|
||||
logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}")
|
||||
moved_reag = relevant_reagents.pop(idx)
|
||||
relevant_reagents.insert(0, moved_reag)
|
||||
else:
|
||||
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
|
||||
logger.debug(f"New relevant reagents: {relevant_reagents}")
|
||||
self.setObjectName(f"lot_{reagent.type}")
|
||||
self.addItems(relevant_reagents)
|
||||
|
||||
@@ -478,7 +478,7 @@ class Result(BaseModel):
|
||||
self.owner = inspect.stack()[1].function
|
||||
|
||||
def report(self):
|
||||
from frontend.custom_widgets.misc import AlertPop
|
||||
from frontend.widgets.misc import AlertPop
|
||||
return AlertPop(message=self.msg, status=self.status, owner=self.owner)
|
||||
|
||||
class Report(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user