Context menu for runs working.
This commit is contained in:
@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from markdown import markdown
|
||||
from pandas import ExcelWriter
|
||||
from backend import Reagent, BasicSample, Organization, KitType, BasicRun
|
||||
from backend import Reagent, Sample, ClientSubmission, KitType, Run
|
||||
from tools import (
|
||||
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
|
||||
under_development
|
||||
@@ -22,7 +22,7 @@ from .date_type_picker import DateTypePicker
|
||||
from .functions import select_save_file
|
||||
from .pop_ups import HTMLPop
|
||||
from .misc import Pagifier
|
||||
from .submission_table import SubmissionsSheet, SubmissionsTree, ClientRunModel
|
||||
from .submission_table import SubmissionsSheet, SubmissionsTree, ClientSubmissionRunModel
|
||||
from .submission_widget import SubmissionFormContainer
|
||||
from .controls_chart import ControlsViewer
|
||||
from .summary import Summary
|
||||
@@ -30,7 +30,7 @@ from .turnaround import TurnaroundTime
|
||||
from .concentrations import Concentrations
|
||||
from .omni_search import SearchBox
|
||||
|
||||
logger = logging.getLogger(f'submissions.{__name__}')
|
||||
logger = logging.getLogger(f'procedure.{__name__}')
|
||||
|
||||
|
||||
class App(QMainWindow):
|
||||
@@ -57,7 +57,7 @@ class App(QMainWindow):
|
||||
# NOTE: insert tabs into main app
|
||||
self.table_widget = AddSubForm(self)
|
||||
self.setCentralWidget(self.table_widget)
|
||||
# NOTE: run initial setups
|
||||
# NOTE: procedure initial setups
|
||||
self._createActions()
|
||||
self._createMenuBar()
|
||||
self._createToolBar()
|
||||
@@ -173,14 +173,14 @@ class App(QMainWindow):
|
||||
|
||||
def runSampleSearch(self):
|
||||
"""
|
||||
Create a search for samples.
|
||||
Create a search for sample.
|
||||
"""
|
||||
dlg = SearchBox(self, object_type=BasicSample, extras=[])
|
||||
dlg = SearchBox(self, object_type=Sample, extras=[])
|
||||
dlg.exec()
|
||||
|
||||
@check_authorization
|
||||
def edit_reagent(self, *args, **kwargs):
|
||||
dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="role")])
|
||||
dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="reagentrole")])
|
||||
dlg.exec()
|
||||
|
||||
def update_data(self):
|
||||
@@ -239,7 +239,7 @@ class AddSubForm(QWidget):
|
||||
self.tabs.addTab(self.tab3, "PCR Controls")
|
||||
self.tabs.addTab(self.tab4, "Cost Report")
|
||||
self.tabs.addTab(self.tab5, "Turnaround Times")
|
||||
# NOTE: Create run adder form
|
||||
# NOTE: Create procedure adder form
|
||||
self.formwidget = SubmissionFormContainer(self)
|
||||
self.formlayout = QVBoxLayout(self)
|
||||
self.formwidget.setLayout(self.formlayout)
|
||||
@@ -249,12 +249,12 @@ class AddSubForm(QWidget):
|
||||
self.interior.setWidgetResizable(True)
|
||||
self.interior.setFixedWidth(325)
|
||||
self.interior.setWidget(self.formwidget)
|
||||
# NOTE: Create sheet to hold existing submissions
|
||||
# NOTE: Create sheet to hold existing procedure
|
||||
self.sheetwidget = QWidget(self)
|
||||
self.sheetlayout = QVBoxLayout(self)
|
||||
self.sheetwidget.setLayout(self.sheetlayout)
|
||||
# self.sub_wid = SubmissionsSheet(parent=parent)
|
||||
self.sub_wid = SubmissionsTree(parent=parent, model=ClientRunModel(self))
|
||||
self.sub_wid = SubmissionsTree(parent=parent, model=ClientSubmissionRunModel(self))
|
||||
self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size)
|
||||
self.sheetlayout.addWidget(self.sub_wid)
|
||||
self.sheetlayout.addWidget(self.pager)
|
||||
@@ -264,11 +264,13 @@ class AddSubForm(QWidget):
|
||||
self.tab1.layout.addWidget(self.interior)
|
||||
self.tab1.layout.addWidget(self.sheetwidget)
|
||||
self.tab2.layout = QVBoxLayout(self)
|
||||
self.irida_viewer = ControlsViewer(self, archetype="Irida Control")
|
||||
# self.irida_viewer = ControlsViewer(self, archetype="Irida Control")
|
||||
self.irida_viewer = None
|
||||
self.tab2.layout.addWidget(self.irida_viewer)
|
||||
self.tab2.setLayout(self.tab2.layout)
|
||||
self.tab3.layout = QVBoxLayout(self)
|
||||
self.pcr_viewer = ControlsViewer(self, archetype="PCR Control")
|
||||
# self.pcr_viewer = ControlsViewer(self, archetype="PCR Control")
|
||||
self.pcr_viewer = None
|
||||
self.tab3.layout.addWidget(self.pcr_viewer)
|
||||
self.tab3.setLayout(self.tab3.layout)
|
||||
summary_report = Summary(self)
|
||||
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend import ChartReportMaker
|
||||
from backend.db import ControlType, IridaControl
|
||||
from backend.db import ControlType
|
||||
import logging
|
||||
from tools import Report, report_result
|
||||
from frontend.visualizations import CustomFigure
|
||||
@@ -25,7 +25,7 @@ class ControlsViewer(InfoPane):
|
||||
return
|
||||
# NOTE: set tab2 layout
|
||||
self.control_sub_typer = QComboBox()
|
||||
# NOTE: fetch types of controls
|
||||
# NOTE: fetch types of control
|
||||
con_sub_types = [item for item in self.archetype.targets.keys()]
|
||||
self.control_sub_typer.addItems(con_sub_types)
|
||||
# NOTE: create custom widget to get types of analysis -- disabled by PCR control
|
||||
@@ -52,7 +52,7 @@ class ControlsViewer(InfoPane):
|
||||
@report_result
|
||||
def update_data(self, *args, **kwargs):
|
||||
"""
|
||||
Get controls based on start/end dates
|
||||
Get control based on start/end dates
|
||||
"""
|
||||
super().update_data()
|
||||
# NOTE: mode_sub_type defaults to disabled
|
||||
@@ -70,7 +70,7 @@ class ControlsViewer(InfoPane):
|
||||
sub_types = []
|
||||
# NOTE: added in allowed to have subtypes in case additions made in future.
|
||||
if sub_types and self.mode.lower() in self.archetype.instance_class.subtyping_allowed:
|
||||
# NOTE: block signal that will rerun controls getter and update mode_sub_typer
|
||||
# NOTE: block signal that will rerun control getter and update mode_sub_typer
|
||||
with QSignalBlocker(self.mode_sub_typer) as blocker:
|
||||
self.mode_sub_typer.addItems(sub_types)
|
||||
self.mode_sub_typer.setEnabled(True)
|
||||
@@ -83,7 +83,7 @@ class ControlsViewer(InfoPane):
|
||||
@report_result
|
||||
def chart_maker_function(self, *args, **kwargs):
|
||||
"""
|
||||
Create html chart for controls reporting
|
||||
Create html chart for control reporting
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -98,7 +98,7 @@ class ControlsViewer(InfoPane):
|
||||
else:
|
||||
self.mode_sub_type = self.mode_sub_typer.currentText()
|
||||
months = self.diff_month(self.start_date, self.end_date)
|
||||
# NOTE: query all controls using the type/start and end dates from the gui
|
||||
# NOTE: query all control using the type/start and end dates from the gui
|
||||
chart_settings = dict(
|
||||
sub_type=self.con_sub_type,
|
||||
start_date=self.start_date,
|
||||
|
||||
@@ -6,7 +6,7 @@ from PyQt6.QtCore import Qt, QSignalBlocker
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
|
||||
)
|
||||
from backend.db.models import Equipment, BasicRun, Process
|
||||
from backend.db.models import Equipment, Run, Process, Procedure
|
||||
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
|
||||
import logging
|
||||
from typing import Generator
|
||||
@@ -16,13 +16,13 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class EquipmentUsage(QDialog):
|
||||
|
||||
def __init__(self, parent, submission: BasicRun):
|
||||
def __init__(self, parent, procedure: Procedure):
|
||||
super().__init__(parent)
|
||||
self.submission = submission
|
||||
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
|
||||
self.used_equipment = self.submission.used_equipment
|
||||
self.kit = self.submission.extraction_kit
|
||||
self.opt_equipment = submission.submission_type.get_equipment()
|
||||
self.procedure = procedure
|
||||
self.setWindowTitle(f"Equipment Checklist - {procedure.rsl_plate_num}")
|
||||
self.used_equipment = self.procedure.equipment
|
||||
self.kit = self.procedure.kittype
|
||||
self.opt_equipment = procedure.proceduretype.get_equipment()
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.populate_form()
|
||||
@@ -120,7 +120,7 @@ class RoleComboBox(QWidget):
|
||||
|
||||
def update_processes(self):
|
||||
"""
|
||||
Changes processes when equipment is changed
|
||||
Changes process when equipment is changed
|
||||
"""
|
||||
equip = self.box.currentText()
|
||||
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
|
||||
@@ -134,10 +134,10 @@ class RoleComboBox(QWidget):
|
||||
"""
|
||||
process = self.process.currentText().strip()
|
||||
process = Process.query(name=process)
|
||||
if process.tip_roles:
|
||||
for iii, tip_role in enumerate(process.tip_roles):
|
||||
if process.tiprole:
|
||||
for iii, tip_role in enumerate(process.tiprole):
|
||||
widget = QComboBox()
|
||||
tip_choices = [item.name for item in tip_role.controls]
|
||||
tip_choices = [item.name for item in tip_role.control]
|
||||
widget.setEditable(False)
|
||||
widget.addItems(tip_choices)
|
||||
widget.setObjectName(f"tips_{tip_role.name}")
|
||||
|
||||
@@ -12,7 +12,7 @@ import logging, numpy as np
|
||||
from pprint import pformat
|
||||
from typing import Tuple, List
|
||||
from pathlib import Path
|
||||
from backend.db.models import BasicRun
|
||||
from backend.db.models import Run
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -20,7 +20,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
# Main window class
|
||||
class GelBox(QDialog):
|
||||
|
||||
def __init__(self, parent, img_path: str | Path, submission: BasicRun):
|
||||
def __init__(self, parent, img_path: str | Path, submission: Run):
|
||||
super().__init__(parent)
|
||||
# NOTE: setting title
|
||||
self.setWindowTitle(f"Gel - {img_path}")
|
||||
@@ -135,7 +135,7 @@ class ControlsForm(QWidget):
|
||||
|
||||
def parse_form(self) -> Tuple[List[dict], str]:
|
||||
"""
|
||||
Pulls the controls statuses from the form.
|
||||
Pulls the control statuses from the form.
|
||||
|
||||
Returns:
|
||||
List[dict]: output of values
|
||||
|
||||
@@ -39,7 +39,7 @@ class InfoPane(QWidget):
|
||||
lastmonth = self.datepicker.end_date.date().addDays(-31)
|
||||
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
|
||||
logger.warning(msg)
|
||||
# NOTE: block signal that will rerun controls getter and set start date without triggering this function again
|
||||
# NOTE: block signal that will rerun control getter and set start date without triggering this function again
|
||||
with QSignalBlocker(self.datepicker.start_date) as blocker:
|
||||
self.datepicker.start_date.setDate(lastmonth)
|
||||
self.update_data()
|
||||
|
||||
@@ -19,7 +19,7 @@ env = jinja_template_loading()
|
||||
|
||||
class StartEndDatePicker(QWidget):
|
||||
"""
|
||||
custom widget to pick start and end dates for controls graphs
|
||||
custom widget to pick start and end dates for control graphs
|
||||
"""
|
||||
|
||||
def __init__(self, default_start: int) -> None:
|
||||
|
||||
@@ -71,7 +71,7 @@ class AddEdit(QDialog):
|
||||
# logger.debug(f"We have an elastic model.")
|
||||
parsed['instance'] = self.instance
|
||||
# NOTE: Hand-off to pydantic model for validation.
|
||||
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
|
||||
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contact.
|
||||
model = model(**parsed)
|
||||
return model, report
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class ManagerWindow(QDialog):
|
||||
|
||||
def update_data(self) -> None:
|
||||
"""
|
||||
Performs updating of widgets on first run and after options change.
|
||||
Performs updating of widgets on first procedure and after options change.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
@@ -100,7 +100,7 @@ class SearchBox(QDialog):
|
||||
|
||||
def update_data(self):
|
||||
"""
|
||||
Shows dataframe of relevant samples.
|
||||
Shows dataframe of relevant sample.
|
||||
"""
|
||||
fields = self.parse_form()
|
||||
sample_list_creator = self.object_type.fuzzy_search(**fields)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
@@ -6,9 +5,12 @@ from PyQt6.QtCore import Qt, pyqtSlot
|
||||
from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout
|
||||
from backend.validators import PydSubmission
|
||||
|
||||
from backend.db.models import ClientSubmission
|
||||
from backend.validators import PydSample, RSLNamer
|
||||
from tools import get_application_from_parent, jinja_template_loading
|
||||
|
||||
|
||||
env = jinja_template_loading()
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -16,9 +18,13 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class SampleChecker(QDialog):
|
||||
|
||||
def __init__(self, parent, title:str, pyd: PydSubmission):
|
||||
def __init__(self, parent, title: str, samples: List[PydSample], clientsubmission: ClientSubmission|None=None):
|
||||
super().__init__(parent)
|
||||
self.pyd = pyd
|
||||
if clientsubmission:
|
||||
self.rsl_plate_num = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
|
||||
else:
|
||||
self.rsl_plate_num = clientsubmission
|
||||
self.samples = samples
|
||||
self.setWindowTitle(title)
|
||||
self.app = get_application_from_parent(parent)
|
||||
self.webview = QWebEngineView(parent=self)
|
||||
@@ -36,9 +42,10 @@ class SampleChecker(QDialog):
|
||||
css = f.read()
|
||||
try:
|
||||
samples = self.formatted_list
|
||||
except AttributeError:
|
||||
except AttributeError as e:
|
||||
logger.error(f"Problem getting sample list: {e}")
|
||||
samples = []
|
||||
html = template.render(samples=samples, css=css)
|
||||
html = template.render(samples=samples, css=css, rsl_plate_num=self.rsl_plate_num)
|
||||
self.webview.setHtml(html)
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
@@ -51,25 +58,37 @@ class SampleChecker(QDialog):
|
||||
@pyqtSlot(str, str, str)
|
||||
def text_changed(self, submission_rank: str, key: str, new_value: str):
|
||||
logger.debug(f"Name: {submission_rank}, Key: {key}, Value: {new_value}")
|
||||
match key:
|
||||
case "row" | "column":
|
||||
value = [new_value]
|
||||
case _:
|
||||
value = new_value
|
||||
try:
|
||||
item = next((sample for sample in self.pyd.samples if int(submission_rank) in sample.submission_rank))
|
||||
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
|
||||
except StopIteration:
|
||||
logger.error(f"Unable to find sample {submission_rank}")
|
||||
return
|
||||
item.__setattr__(key, value)
|
||||
item.__setattr__(key, new_value)
|
||||
|
||||
@pyqtSlot(str, bool)
|
||||
def enable_sample(self, submission_rank: str, enabled: bool):
|
||||
logger.debug(f"Name: {submission_rank}, Enabled: {enabled}")
|
||||
try:
|
||||
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
|
||||
except StopIteration:
|
||||
logger.error(f"Unable to find sample {submission_rank}")
|
||||
return
|
||||
item.__setattr__("enabled", enabled)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_rsl_plate_num(self, rsl_plate_num: str):
|
||||
logger.debug(f"RSL plate num: {rsl_plate_num}")
|
||||
self.rsl_plate_num = rsl_plate_num
|
||||
|
||||
@property
|
||||
def formatted_list(self) -> List[dict]:
|
||||
output = []
|
||||
for sample in self.pyd.sample_list:
|
||||
if sample['submitter_id'] in [item['submitter_id'] for item in output]:
|
||||
sample['color'] = "red"
|
||||
for sample in self.samples:
|
||||
logger.debug(sample)
|
||||
s = sample.improved_dict(dictionaries=False)
|
||||
if s['sample_id'] in [item['sample_id'] for item in output]:
|
||||
s['color'] = "red"
|
||||
else:
|
||||
sample['color'] = "black"
|
||||
output.append(sample)
|
||||
s['color'] = "black"
|
||||
output.append(s)
|
||||
return output
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Webview to show run and sample details.
|
||||
Webview to show procedure and sample details.
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
|
||||
QDialogButtonBox, QTextEdit, QGridLayout)
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtCore import Qt, pyqtSlot
|
||||
from jinja2 import TemplateNotFound
|
||||
from backend.db.models import BasicRun, BasicSample, Reagent, KitType, Equipment, Process, Tips
|
||||
from backend.db.models import Run, Sample, Reagent, KitType, Equipment, Process, Tips
|
||||
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
|
||||
from .functions import select_save_file, save_pdf
|
||||
from pathlib import Path
|
||||
@@ -18,15 +18,15 @@ from pprint import pformat
|
||||
from typing import List
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
|
||||
class SubmissionDetails(QDialog):
|
||||
"""
|
||||
a window showing text details of run
|
||||
a window showing text details of procedure
|
||||
"""
|
||||
|
||||
def __init__(self, parent, sub: BasicRun | BasicSample | Reagent) -> None:
|
||||
def __init__(self, parent, sub: Run | Sample | Reagent) -> None:
|
||||
|
||||
super().__init__(parent)
|
||||
self.app = get_application_from_parent(parent)
|
||||
@@ -51,10 +51,10 @@ class SubmissionDetails(QDialog):
|
||||
self.channel = QWebChannel()
|
||||
self.channel.registerObject('backend', self)
|
||||
match sub:
|
||||
case BasicRun():
|
||||
case Run():
|
||||
self.run_details(run=sub)
|
||||
self.rsl_plate_num = sub.rsl_plate_num
|
||||
case BasicSample():
|
||||
case Sample():
|
||||
self.sample_details(sample=sub)
|
||||
case Reagent():
|
||||
self.reagent_details(reagent=sub)
|
||||
@@ -127,7 +127,7 @@ class SubmissionDetails(QDialog):
|
||||
self.setWindowTitle(f"Process Details - {tips.name}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sample_details(self, sample: str | BasicSample):
|
||||
def sample_details(self, sample: str | Sample):
|
||||
"""
|
||||
Changes details view to summary of Sample
|
||||
|
||||
@@ -136,19 +136,19 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
logger.debug(f"Sample details.")
|
||||
if isinstance(sample, str):
|
||||
sample = BasicSample.query(submitter_id=sample)
|
||||
sample = Sample.query(sample_id=sample)
|
||||
base_dict = sample.to_sub_dict(full_data=True)
|
||||
exclude = ['submissions', 'excluded', 'colour', 'tooltip']
|
||||
exclude = ['procedure', 'excluded', 'colour', 'tooltip']
|
||||
base_dict['excluded'] = exclude
|
||||
template = sample.details_template
|
||||
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
html = template.render(sample=base_dict, css=css)
|
||||
# with open(f"{sample.submitter_id}.html", 'w') as f:
|
||||
# with open(f"{sample.sample_id}.html", 'w') as f:
|
||||
# f.write(html)
|
||||
self.webview.setHtml(html)
|
||||
self.setWindowTitle(f"Sample Details - {sample.submitter_id}")
|
||||
self.setWindowTitle(f"Sample Details - {sample.sample_id}")
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
|
||||
@@ -156,7 +156,7 @@ class SubmissionDetails(QDialog):
|
||||
Changes details view to summary of Reagent
|
||||
|
||||
Args:
|
||||
kit (str | KitType): Name of kit.
|
||||
kit (str | KitType): Name of kittype.
|
||||
reagent (str | Reagent): Lot number of the reagent
|
||||
"""
|
||||
logger.debug(f"Reagent details.")
|
||||
@@ -164,7 +164,7 @@ class SubmissionDetails(QDialog):
|
||||
reagent = Reagent.query(lot=reagent)
|
||||
if isinstance(kit, str):
|
||||
self.kit = KitType.query(name=kit)
|
||||
base_dict = reagent.to_sub_dict(extraction_kit=self.kit, full_data=True)
|
||||
base_dict = reagent.to_sub_dict(kittype=self.kit, full_data=True)
|
||||
env = jinja_template_loading()
|
||||
temp_name = "reagent_details.html"
|
||||
try:
|
||||
@@ -203,7 +203,7 @@ class SubmissionDetails(QDialog):
|
||||
logger.error(f"Reagent with lot {old_lot} not found.")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def run_details(self, run: str | BasicRun):
|
||||
def run_details(self, run: str | Run):
|
||||
"""
|
||||
Sets details view to summary of Submission.
|
||||
|
||||
@@ -212,24 +212,24 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
logger.debug(f"Submission details.")
|
||||
if isinstance(run, str):
|
||||
run = BasicRun.query(rsl_plate_num=run)
|
||||
run = Run.query(name=run)
|
||||
self.rsl_plate_num = run.rsl_plate_num
|
||||
self.base_dict = run.to_dict(full_data=True)
|
||||
# NOTE: don't want id
|
||||
self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked)
|
||||
self.base_dict['excluded'] = run.get_default_info("details_ignore")
|
||||
self.base_dict, self.template = run.get_details_template(base_dict=self.base_dict)
|
||||
self.template = run.details_template
|
||||
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
# logger.debug(f"Base dictionary of run {self.rsl_plate_num}: {pformat(self.base_dict)}")
|
||||
# logger.debug(f"Base dictionary of procedure {self.name}: {pformat(self.base_dict)}")
|
||||
self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
|
||||
self.webview.setHtml(self.html)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sign_off(self, run: str | BasicRun) -> None:
|
||||
def sign_off(self, run: str | Run) -> None:
|
||||
"""
|
||||
Allows power user to signify a run is complete.
|
||||
Allows power user to signify a procedure is complete.
|
||||
|
||||
Args:
|
||||
run (str | BasicRun): Submission to be completed
|
||||
@@ -239,7 +239,7 @@ class SubmissionDetails(QDialog):
|
||||
"""
|
||||
logger.info(f"Signing off on {run} - ({getuser()})")
|
||||
if isinstance(run, str):
|
||||
run = BasicRun.query(rsl_plate_num=run)
|
||||
run = Run.query(name=run)
|
||||
run.signed_by = getuser()
|
||||
run.completed_date = datetime.now()
|
||||
run.completed_date.replace(tzinfo=timezone)
|
||||
@@ -248,7 +248,7 @@ class SubmissionDetails(QDialog):
|
||||
|
||||
def save_pdf(self):
|
||||
"""
|
||||
Renders run to html, then creates and saves .pdf file to user selected file.
|
||||
Renders procedure to html, then creates and saves .pdf file to user selected file.
|
||||
"""
|
||||
fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf")
|
||||
save_pdf(obj=self.webview, filename=fname)
|
||||
@@ -256,11 +256,11 @@ class SubmissionDetails(QDialog):
|
||||
|
||||
class SubmissionComment(QDialog):
|
||||
"""
|
||||
a window for adding comment text to a run
|
||||
a window for adding comment text to a procedure
|
||||
"""
|
||||
|
||||
def __init__(self, parent, submission: BasicRun) -> None:
|
||||
|
||||
def __init__(self, parent, submission: Run) -> None:
|
||||
logger.debug(parent)
|
||||
super().__init__(parent)
|
||||
self.app = get_application_from_parent(parent)
|
||||
self.submission = submission
|
||||
@@ -282,7 +282,7 @@ class SubmissionComment(QDialog):
|
||||
|
||||
def parse_form(self) -> List[dict]:
|
||||
"""
|
||||
Adds comment to run object.
|
||||
Adds comment to procedure object.
|
||||
"""
|
||||
commenter = getuser()
|
||||
comment = self.txt_editor.toPlainText()
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
"""
|
||||
Contains widgets specific to the run summary and run details.
|
||||
Contains widgets specific to the procedure summary and procedure details.
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import sys, logging, re
|
||||
from pprint import pformat
|
||||
|
||||
from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \
|
||||
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
|
||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
|
||||
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor
|
||||
from backend.db.models import BasicRun, ClientSubmission
|
||||
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent
|
||||
|
||||
from backend.db.models import Run, ClientSubmission
|
||||
from tools import Report, Result, report_result
|
||||
from .functions import select_open_file
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
logger = logging.getLogger(f"procedure.{__name__}")
|
||||
|
||||
|
||||
class pandasModel(QAbstractTableModel):
|
||||
@@ -63,7 +65,7 @@ class pandasModel(QAbstractTableModel):
|
||||
|
||||
class SubmissionsSheet(QTableView):
|
||||
"""
|
||||
presents run summary to user in tab1
|
||||
presents procedure summary to user in tab1
|
||||
"""
|
||||
|
||||
def __init__(self, parent) -> None:
|
||||
@@ -78,16 +80,16 @@ class SubmissionsSheet(QTableView):
|
||||
self.resizeColumnsToContents()
|
||||
self.resizeRowsToContents()
|
||||
self.setSortingEnabled(True)
|
||||
self.doubleClicked.connect(lambda x: BasicRun.query(id=x.sibling(x.row(), 0).data()).show_details(self))
|
||||
# NOTE: Have to run native query here because mine just returns results?
|
||||
self.total_count = BasicRun.__database_session__.query(BasicRun).count()
|
||||
self.doubleClicked.connect(lambda x: Run.query(id=x.sibling(x.row(), 0).data()).show_details(self))
|
||||
# NOTE: Have to procedure native query here because mine just returns results?
|
||||
self.total_count = Run.__database_session__.query(Run).count()
|
||||
|
||||
def set_data(self, page: int = 1, page_size: int = 250) -> None:
|
||||
"""
|
||||
sets data in model
|
||||
"""
|
||||
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
|
||||
self.data = BasicRun.submissions_to_df(page=page, page_size=page_size)
|
||||
self.data = Run.submissions_to_df(page=page, page_size=page_size)
|
||||
try:
|
||||
self.data['Id'] = self.data['Id'].apply(str)
|
||||
self.data['Id'] = self.data['Id'].str.zfill(4)
|
||||
@@ -108,7 +110,7 @@ class SubmissionsSheet(QTableView):
|
||||
id = self.selectionModel().currentIndex()
|
||||
# NOTE: Convert to data in id column (i.e. column 0)
|
||||
id = id.sibling(id.row(), 0).data()
|
||||
submission = BasicRun.query(id=id)
|
||||
submission = Run.query(id=id)
|
||||
self.menu = QMenu(self)
|
||||
self.con_actions = submission.custom_context_events()
|
||||
for k in self.con_actions.keys():
|
||||
@@ -140,7 +142,7 @@ class SubmissionsSheet(QTableView):
|
||||
|
||||
def link_extractions_function(self):
|
||||
"""
|
||||
Link extractions from runlogs to imported submissions
|
||||
Link extractions from runlogs to imported procedure
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -166,9 +168,9 @@ class SubmissionsSheet(QTableView):
|
||||
# NOTE: 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]
|
||||
# NOTE: Lookup imported submissions
|
||||
sub = BasicRun.query(rsl_plate_num=new_run['rsl_plate_num'])
|
||||
# NOTE: If no such run exists, move onto the next run
|
||||
# NOTE: Lookup imported procedure
|
||||
sub = Run.query(name=new_run['name'])
|
||||
# NOTE: If no such procedure exists, move onto the next procedure
|
||||
if sub is None:
|
||||
continue
|
||||
try:
|
||||
@@ -192,7 +194,7 @@ class SubmissionsSheet(QTableView):
|
||||
|
||||
def link_pcr_function(self):
|
||||
"""
|
||||
Link PCR data from run logs to an imported run
|
||||
Link PCR data from procedure logs to an imported procedure
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -215,9 +217,9 @@ class SubmissionsSheet(QTableView):
|
||||
experiment_name=run[4].strip(),
|
||||
end_time=run[5].strip()
|
||||
)
|
||||
# NOTE: lookup imported run
|
||||
sub = BasicRun.query(rsl_number=new_run['rsl_plate_num'])
|
||||
# NOTE: if imported run doesn't exist move on to next run
|
||||
# NOTE: lookup imported procedure
|
||||
sub = Run.query(rsl_number=new_run['name'])
|
||||
# NOTE: if imported procedure doesn't exist move on to next procedure
|
||||
if sub is None:
|
||||
continue
|
||||
sub.set_attribute('pcr_info', new_run)
|
||||
@@ -227,9 +229,10 @@ class SubmissionsSheet(QTableView):
|
||||
return report
|
||||
|
||||
|
||||
class RunDelegate(QStyledItemDelegate):
|
||||
class ClientSubmissionDelegate(QStyledItemDelegate):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(RunDelegate, self).__init__(parent)
|
||||
super(ClientSubmissionDelegate, self).__init__(parent)
|
||||
pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
|
||||
icon1 = QWidget().style().standardIcon(pixmapi)
|
||||
pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
|
||||
@@ -238,23 +241,29 @@ class RunDelegate(QStyledItemDelegate):
|
||||
self._minus_icon = icon2
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super(RunDelegate, self).initStyleOption(option, index)
|
||||
super(ClientSubmissionDelegate, self).initStyleOption(option, index)
|
||||
if not index.parent().isValid():
|
||||
is_open = bool(option.state & QStyle.StateFlag.State_Open)
|
||||
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
|
||||
option.icon = self._minus_icon if is_open else self._plus_icon
|
||||
|
||||
|
||||
class RunDelegate(ClientSubmissionDelegate):
|
||||
pass
|
||||
|
||||
|
||||
class SubmissionsTree(QTreeView):
|
||||
"""
|
||||
https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt
|
||||
"""
|
||||
|
||||
def __init__(self, model, parent=None):
|
||||
super(SubmissionsTree, self).__init__(parent)
|
||||
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
|
||||
self.setIndentation(0)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.clicked.connect(self.on_clicked)
|
||||
delegate = RunDelegate(self)
|
||||
delegate = ClientSubmissionDelegate(self)
|
||||
self.setItemDelegateForColumn(0, delegate)
|
||||
self.model = model
|
||||
self.setModel(self.model)
|
||||
@@ -263,32 +272,69 @@ class SubmissionsTree(QTreeView):
|
||||
# self.setStyleSheet("background-color: #0D1225;")
|
||||
self.set_data()
|
||||
self.doubleClicked.connect(self.show_details)
|
||||
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
# self.customContextMenuRequested.connect(self.open_menu)
|
||||
|
||||
for ii in range(2):
|
||||
self.resizeColumnToContents(ii)
|
||||
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def on_clicked(self, index):
|
||||
if not index.parent().isValid() and index.column() == 0:
|
||||
self.setExpanded(index, not self.isExpanded(index))
|
||||
|
||||
def contextMenuEvent(self, event: QContextMenuEvent):
|
||||
"""
|
||||
Creates actions for right click menu events.
|
||||
|
||||
Args:
|
||||
event (_type_): the item of interest
|
||||
"""
|
||||
indexes = self.selectedIndexes()
|
||||
|
||||
dicto = next((item.data(1) for item in indexes if item.data(1)))
|
||||
query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
|
||||
logger.debug(query_obj)
|
||||
|
||||
# NOTE: Convert to data in id column (i.e. column 0)
|
||||
# id = id.sibling(id.row(), 0).data()
|
||||
|
||||
# logger.debug(id.model().query_group_object(id.row()))
|
||||
# clientsubmission = id.model().query_group_object(id.row())
|
||||
self.menu = QMenu(self)
|
||||
self.con_actions = query_obj.custom_context_events
|
||||
for key in self.con_actions.keys():
|
||||
if key.lower() == "add procedure":
|
||||
action = QMenu(self.menu)
|
||||
action.setTitle("Add Procedure")
|
||||
for procedure in query_obj.allowed_procedures:
|
||||
proc_name = procedure.name
|
||||
proc = QAction(proc_name, action)
|
||||
proc.triggered.connect(lambda _, procedure_name=proc_name: self.con_actions['Add Procedure'](obj=self, proceduretype_name=procedure_name))
|
||||
action.addAction(proc)
|
||||
self.menu.addMenu(action)
|
||||
else:
|
||||
action = QAction(key, self)
|
||||
action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self))
|
||||
self.menu.addAction(action)
|
||||
# # NOTE: add other required actions
|
||||
self.menu.popup(QCursor.pos())
|
||||
|
||||
def set_data(self, page: int = 1, page_size: int = 250) -> None:
|
||||
"""
|
||||
sets data in model
|
||||
"""
|
||||
self.clear()
|
||||
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
|
||||
self.data = [item.to_dict(full_data=True) for item in ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
|
||||
logger.debug(pformat(self.data))
|
||||
self.data = [item.to_dict(full_data=True) for item in
|
||||
ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
|
||||
logger.debug(f"setting data:\n {pformat(self.data)}")
|
||||
# sys.exit()
|
||||
for submission in self.data:
|
||||
group_str = f"{submission['submission_type']}-{submission['submitter_plate_number']}-{submission['submitted_date']}"
|
||||
group_item = self.model.add_group(group_str)
|
||||
for run in submission['runs']:
|
||||
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
|
||||
group_item = self.model.add_group(group_str, query_str=submission['submitter_plate_id'])
|
||||
for run in submission['run']:
|
||||
self.model.append_element_to_group(group_item=group_item, element=run)
|
||||
|
||||
|
||||
def clear(self):
|
||||
if self.model != None:
|
||||
# self.model.clear() # works
|
||||
@@ -302,8 +348,7 @@ class SubmissionsTree(QTreeView):
|
||||
id = int(id.data())
|
||||
except ValueError:
|
||||
return
|
||||
BasicRun.query(id=id).show_details(self)
|
||||
|
||||
Run.query(id=id).show_details(self)
|
||||
|
||||
def link_extractions(self):
|
||||
pass
|
||||
@@ -312,62 +357,64 @@ class SubmissionsTree(QTreeView):
|
||||
pass
|
||||
|
||||
|
||||
class ClientRunModel(QStandardItemModel):
|
||||
class ClientSubmissionRunModel(QStandardItemModel):
|
||||
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ClientRunModel, self).__init__(parent)
|
||||
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Technician", "Signed By"]
|
||||
super(ClientSubmissionRunModel, self).__init__(parent)
|
||||
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
|
||||
self.setColumnCount(len(headers))
|
||||
self.setHorizontalHeaderLabels(headers)
|
||||
|
||||
for i in range(self.columnCount()):
|
||||
it = self.horizontalHeaderItem(i)
|
||||
try:
|
||||
logger.debug(it.text())
|
||||
except AttributeError:
|
||||
pass
|
||||
# it.setForeground(QColor("#F2F2F2"))
|
||||
|
||||
def add_group(self, group_name):
|
||||
def add_group(self, item_name, query_str: str):
|
||||
item_root = QStandardItem()
|
||||
item_root.setEditable(False)
|
||||
item = QStandardItem(group_name)
|
||||
item = QStandardItem(item_name)
|
||||
item.setEditable(False)
|
||||
ii = self.invisibleRootItem()
|
||||
i = ii.rowCount()
|
||||
for j, it in enumerate((item_root, item)):
|
||||
# NOTE: Adding item to invisible root row i, column j (wherever j comes from)
|
||||
ii.setChild(i, j, it)
|
||||
ii.setEditable(False)
|
||||
for j in range(self.columnCount()):
|
||||
it = ii.child(i, j)
|
||||
if it is None:
|
||||
# NOTE: Set invisible root child to empty if it is None.
|
||||
it = QStandardItem()
|
||||
ii.setChild(i, j, it)
|
||||
# it.setBackground(QColor("#002842"))
|
||||
# it.setForeground(QColor("#F2F2F2"))
|
||||
item_root.setData(dict(item_type=ClientSubmission, query_str=query_str), 1)
|
||||
return item_root
|
||||
|
||||
def append_element_to_group(self, group_item, element:dict):
|
||||
logger.debug(f"Element: {pformat(element)}")
|
||||
def append_element_to_group(self, group_item, element: dict):
|
||||
# logger.debug(f"Element: {pformat(element)}")
|
||||
j = group_item.rowCount()
|
||||
item_icon = QStandardItem()
|
||||
item_icon.setEditable(False)
|
||||
|
||||
# item_icon.setBackground(QColor("#0D1225"))
|
||||
# item_icon.setData(dict(item_type="Run", query_str=element['plate_number']), 1)
|
||||
# group_item.setChild(j, 0, item_icon)
|
||||
for i in range(self.columnCount()):
|
||||
it = self.horizontalHeaderItem(i)
|
||||
try:
|
||||
key = it.text().lower().replace(" ", "_")
|
||||
except AttributeError:
|
||||
continue
|
||||
key = None
|
||||
if not key:
|
||||
continue
|
||||
value = str(element[key])
|
||||
item = QStandardItem(value)
|
||||
item.setBackground(QColor("#CFE2F3"))
|
||||
item.setEditable(False)
|
||||
# item_icon.setChild(j, i, item)
|
||||
item.setData(dict(item_type=Run, query_str=element['plate_number']),1)
|
||||
group_item.setChild(j, i, item)
|
||||
# group_item.setChild(j, 1, QStandardItem("B"))
|
||||
|
||||
def get_value(self, idx: int, column: int = 1):
|
||||
return self.item(idx, column)
|
||||
|
||||
def query_group_object(self, idx: int):
|
||||
row_obj = self.get_value(idx)
|
||||
logger.debug(row_obj.query_str)
|
||||
return self.sql_object.query(name=row_obj.query_str, limit=1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Contains all run related frontend functions
|
||||
Contains all procedure related frontend functions
|
||||
"""
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout,
|
||||
@@ -10,11 +10,11 @@ from .functions import select_open_file, select_save_file
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
|
||||
from backend.excel import SheetParser, InfoParser
|
||||
from backend.validators import PydSubmission, PydReagent
|
||||
from backend.excel import ClientSubmissionParser, SampleParser
|
||||
from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample
|
||||
from backend.db import (
|
||||
Organization, SubmissionType, Reagent,
|
||||
ReagentRole, KitTypeReagentRoleAssociation, BasicRun
|
||||
ClientLab, SubmissionType, Reagent,
|
||||
ReagentRole, KitTypeReagentRoleAssociation, Run
|
||||
)
|
||||
from pprint import pformat
|
||||
from .pop_ups import QuestionAsker, AlertPop
|
||||
@@ -93,7 +93,7 @@ class SubmissionFormContainer(QWidget):
|
||||
@report_result
|
||||
def import_submission_function(self, fname: Path | None = None) -> Report:
|
||||
"""
|
||||
Import a new run to the app window
|
||||
Import a new procedure to the app window
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): original app window
|
||||
@@ -110,7 +110,7 @@ class SubmissionFormContainer(QWidget):
|
||||
self.form.setParent(None)
|
||||
except AttributeError:
|
||||
pass
|
||||
# NOTE: initialize samples
|
||||
# NOTE: initialize sample
|
||||
self.samples = []
|
||||
self.missing_info = []
|
||||
# NOTE: set file dialog
|
||||
@@ -121,19 +121,28 @@ class SubmissionFormContainer(QWidget):
|
||||
return report
|
||||
# NOTE: create sheetparser using excel sheet and context from gui
|
||||
try:
|
||||
# self.prsr = SheetParser(filepath=fname)
|
||||
self.parser = InfoParser(filepath=fname)
|
||||
self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
|
||||
except PermissionError:
|
||||
logger.error(f"Couldn't get permission to access file: {fname}")
|
||||
return
|
||||
except AttributeError:
|
||||
self.parser = InfoParser(filepath=fname)
|
||||
self.pyd = self.parser.to_pydantic()
|
||||
# logger.debug(f"Samples: {pformat(self.pyd.samples)}")
|
||||
checker = SampleChecker(self, "Sample Checker", self.pyd)
|
||||
self.clientsubmissionparser = ClientSubmissionParser(filepath=fname)
|
||||
try:
|
||||
# self.prsr = SheetParser(filepath=fname)
|
||||
self.sampleparser = SampleParser(filepath=fname)
|
||||
except PermissionError:
|
||||
logger.error(f"Couldn't get permission to access file: {fname}")
|
||||
return
|
||||
except AttributeError:
|
||||
self.sampleparser = SampleParser(filepath=fname)
|
||||
self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
|
||||
self.pydsamples = self.sampleparser.to_pydantic()
|
||||
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
|
||||
checker = SampleChecker(self, "Sample Checker", self.pydsamples)
|
||||
if checker.exec():
|
||||
# logger.debug(pformat(self.pyd.samples))
|
||||
self.form = self.pyd.to_form(parent=self)
|
||||
# logger.debug(pformat(self.pydclientsubmission.sample))
|
||||
self.form = self.pydclientsubmission.to_form(parent=self)
|
||||
self.form.samples = self.pydsamples
|
||||
self.layout().addWidget(self.form)
|
||||
else:
|
||||
message = "Submission cancelled."
|
||||
@@ -150,7 +159,7 @@ class SubmissionFormContainer(QWidget):
|
||||
instance (Reagent | None): Blank reagent instance to be edited and then added.
|
||||
|
||||
Returns:
|
||||
models.Reagent: the constructed reagent object to add to run
|
||||
models.Reagent: the constructed reagent object to add to procedure
|
||||
"""
|
||||
report = Report()
|
||||
if not instance:
|
||||
@@ -167,23 +176,23 @@ class SubmissionFormContainer(QWidget):
|
||||
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
update_reagent_fields = ['extraction_kit']
|
||||
update_reagent_fields = ['kittype']
|
||||
|
||||
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
|
||||
def __init__(self, parent: QWidget, pyd: PydSubmission, disable: list | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
if disable is None:
|
||||
disable = []
|
||||
self.app = get_application_from_parent(parent)
|
||||
self.pyd = submission
|
||||
self.pyd = pyd
|
||||
self.missing_info = []
|
||||
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
|
||||
basic_submission_class = self.submission_type.submission_class
|
||||
logger.debug(f"Basic run class: {basic_submission_class}")
|
||||
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
|
||||
self.submissiontype = SubmissionType.query(name=self.pyd.submissiontype['value'])
|
||||
# basic_submission_class = self.submission_type.submission_class
|
||||
# logger.debug(f"Basic procedure class: {basic_submission_class}")
|
||||
defaults = Run.get_default_info("form_recover", "form_ignore", submissiontype=self.pyd.submissiontype['value'])
|
||||
self.recover = defaults['form_recover']
|
||||
self.ignore = defaults['form_ignore']
|
||||
self.layout = QVBoxLayout()
|
||||
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
||||
for k in list(self.pyd.model_fields.keys()):# + list(self.pyd.model_extra.keys()):
|
||||
logger.debug(f"Pydantic field: {k}")
|
||||
if k in self.ignore:
|
||||
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
|
||||
@@ -201,8 +210,8 @@ class SubmissionFormWidget(QWidget):
|
||||
except KeyError:
|
||||
value = dict(value=None, missing=True)
|
||||
logger.debug(f"Pydantic value: {value}")
|
||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
|
||||
run_object=basic_submission_class, disable=check)
|
||||
add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype,
|
||||
run_object=Run(), disable=check)
|
||||
if add_widget is not None:
|
||||
self.layout.addWidget(add_widget)
|
||||
if k in self.__class__.update_reagent_fields:
|
||||
@@ -212,7 +221,7 @@ class SubmissionFormWidget(QWidget):
|
||||
self.layout.addWidget(self.disabler)
|
||||
self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
|
||||
self.setStyleSheet(main_form_style)
|
||||
# self.scrape_reagents(self.extraction_kit)
|
||||
# self.scrape_reagents(self.kittype)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def disable_reagents(self):
|
||||
@@ -223,7 +232,7 @@ class SubmissionFormWidget(QWidget):
|
||||
reagent.flip_check(self.disabler.checkbox.isChecked())
|
||||
|
||||
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None,
|
||||
extraction_kit: str | None = None, run_object: BasicRun | None = None,
|
||||
extraction_kit: str | None = None, run_object: Run | None = None,
|
||||
disable: bool = False) -> "self.InfoItem":
|
||||
"""
|
||||
Make an InfoItem widget to hold a field
|
||||
@@ -256,14 +265,14 @@ class SubmissionFormWidget(QWidget):
|
||||
return None
|
||||
|
||||
@report_result
|
||||
def scrape_reagents(self, *args, **kwargs): #extraction_kit:str, caller:str|None=None):
|
||||
def scrape_reagents(self, *args, **kwargs): #kittype:str, caller:str|None=None):
|
||||
"""
|
||||
Extracted scrape reagents function that will run when
|
||||
form 'extraction_kit' widget is updated.
|
||||
Extracted scrape reagents function that will procedure when
|
||||
form 'kittype' widget is updated.
|
||||
|
||||
Args:
|
||||
obj (QMainWindow): updated main application
|
||||
extraction_kit (str): name of extraction kit (in 'extraction_kit' widget)
|
||||
extraction_kit (str): name of extraction kittype (in 'kittype' widget)
|
||||
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Updated application and result
|
||||
@@ -373,7 +382,7 @@ class SubmissionFormWidget(QWidget):
|
||||
return report
|
||||
case _:
|
||||
pass
|
||||
# NOTE: add reagents to run object
|
||||
# NOTE: add reagents to procedure object
|
||||
if base_submission is None:
|
||||
return
|
||||
for reagent in base_submission.reagents:
|
||||
@@ -393,7 +402,7 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
def export_csv_function(self, fname: Path | None = None):
|
||||
"""
|
||||
Save the run's csv file.
|
||||
Save the procedure's csv file.
|
||||
|
||||
Args:
|
||||
fname (Path | None, optional): Input filename. Defaults to None.
|
||||
@@ -405,7 +414,7 @@ class SubmissionFormWidget(QWidget):
|
||||
except PermissionError:
|
||||
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||
except AttributeError:
|
||||
logger.error(f"No csv file found in the run at this point.")
|
||||
logger.error(f"No csv file found in the procedure at this point.")
|
||||
|
||||
def parse_form(self) -> Report:
|
||||
"""
|
||||
@@ -442,11 +451,10 @@ class SubmissionFormWidget(QWidget):
|
||||
report.add_result(report)
|
||||
return report
|
||||
|
||||
|
||||
class InfoItem(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
|
||||
run_object: BasicRun | None = None) -> None:
|
||||
run_object: Run | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
@@ -492,7 +500,7 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
def set_widget(self, parent: QWidget, key: str, value: dict,
|
||||
submission_type: str | SubmissionType | None = None,
|
||||
sub_obj: BasicRun | None = None) -> QWidget:
|
||||
sub_obj: Run | None = None) -> QWidget:
|
||||
"""
|
||||
Creates form widget
|
||||
|
||||
@@ -515,16 +523,16 @@ class SubmissionFormWidget(QWidget):
|
||||
pass
|
||||
obj = parent.parent().parent()
|
||||
match key:
|
||||
case 'submitting_lab':
|
||||
case 'clientlab':
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
labs = [item.name for item in Organization.query()]
|
||||
# NOTE: lookup organizations suitable for clientlab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
labs = [item.name for item in ClientLab.query()]
|
||||
if isinstance(value, dict):
|
||||
value = value['value']
|
||||
if isinstance(value, Organization):
|
||||
if isinstance(value, ClientLab):
|
||||
value = value.name
|
||||
try:
|
||||
looked_up_lab = Organization.query(name=value, limit=1)
|
||||
looked_up_lab = ClientLab.query(name=value, limit=1)
|
||||
except AttributeError:
|
||||
looked_up_lab = None
|
||||
if looked_up_lab:
|
||||
@@ -536,28 +544,28 @@ class SubmissionFormWidget(QWidget):
|
||||
# NOTE: set combobox values to lookedup values
|
||||
add_widget.addItems(labs)
|
||||
add_widget.setToolTip("Select submitting lab.")
|
||||
case 'extraction_kit':
|
||||
# NOTE: if extraction kit not available, all other values fail
|
||||
case 'kittype':
|
||||
# NOTE: if extraction kittype 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!",
|
||||
msg = AlertPop(message="Make sure to check your extraction kittype in the excel sheet!",
|
||||
status="warning")
|
||||
msg.exec()
|
||||
# NOTE: create combobox to hold looked up kits
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
# NOTE: lookup existing kits by 'proceduretype' decided on by sheetparser
|
||||
uses = [item.name for item in submission_type.kit_types]
|
||||
obj.uses = uses
|
||||
if check_not_nan(value):
|
||||
try:
|
||||
uses.insert(0, uses.pop(uses.index(value)))
|
||||
except ValueError:
|
||||
logger.warning(f"Couldn't find kit in list, skipping move to top of list.")
|
||||
logger.warning(f"Couldn't find kittype in list, skipping move to top of list.")
|
||||
obj.ext_kit = value
|
||||
else:
|
||||
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
||||
logger.error(f"Couldn't find {obj.prsr.sub['kittype']}")
|
||||
obj.ext_kit = uses[0]
|
||||
add_widget.addItems(uses)
|
||||
add_widget.setToolTip("Select extraction kit.")
|
||||
add_widget.setToolTip("Select extraction kittype.")
|
||||
parent.extraction_kit = add_widget.currentText()
|
||||
case 'submission_category':
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
@@ -568,7 +576,7 @@ class SubmissionFormWidget(QWidget):
|
||||
except ValueError:
|
||||
categories.insert(0, categories.pop(categories.index(submission_type)))
|
||||
add_widget.addItems(categories)
|
||||
add_widget.setToolTip("Enter run category or select from list.")
|
||||
add_widget.setToolTip("Enter procedure category or select from list.")
|
||||
case _:
|
||||
if key in sub_obj.timestamps:
|
||||
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
|
||||
@@ -692,10 +700,10 @@ class SubmissionFormWidget(QWidget):
|
||||
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
|
||||
return wanted_reagent, report
|
||||
else:
|
||||
# NOTE: In this case we will have an empty reagent and the run will fail kit integrity check
|
||||
# NOTE: In this case we will have an empty reagent and the procedure will fail kittype integrity check
|
||||
return None, report
|
||||
else:
|
||||
# NOTE: 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.
|
||||
# NOTE: Since this now gets passed in directly from the clientsubmissionparser -> pydclientsubmission -> form and the clientsubmissionparser gets the name from the db, it should no longer be necessary to query the db with reagent/kittype, but with rt name directly.
|
||||
rt = ReagentRole.query(name=self.reagent.role)
|
||||
if rt is None:
|
||||
rt = ReagentRole.query(kittype=self.extraction_kit, reagent=wanted_reagent)
|
||||
@@ -738,7 +746,7 @@ class SubmissionFormWidget(QWidget):
|
||||
def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None:
|
||||
super().__init__(scrollWidget=scrollWidget)
|
||||
self.setEditable(True)
|
||||
looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.role,
|
||||
looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.equipmentrole,
|
||||
kittype=extraction_kit)
|
||||
relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()]
|
||||
# NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows
|
||||
@@ -754,7 +762,8 @@ class SubmissionFormWidget(QWidget):
|
||||
looked_up_reg = None
|
||||
if looked_up_reg:
|
||||
try:
|
||||
relevant_reagents.insert(0, relevant_reagents.pop(relevant_reagents.index(looked_up_reg.lot)))
|
||||
relevant_reagents.insert(0, relevant_reagents.pop(
|
||||
relevant_reagents.index(looked_up_reg.lot)))
|
||||
except ValueError as e:
|
||||
logger.error(f"Error reordering relevant reagents: {e}")
|
||||
else:
|
||||
@@ -764,9 +773,9 @@ class SubmissionFormWidget(QWidget):
|
||||
relevant_reagents.insert(0, moved_reag)
|
||||
else:
|
||||
pass
|
||||
self.setObjectName(f"lot_{reagent.role}")
|
||||
self.setObjectName(f"lot_{reagent.equipmentrole}")
|
||||
self.addItems(relevant_reagents)
|
||||
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}")
|
||||
self.setToolTip(f"Enter lot number for the reagent used for {reagent.equipmentrole}")
|
||||
|
||||
class DisableReagents(QWidget):
|
||||
|
||||
@@ -783,16 +792,22 @@ class SubmissionFormWidget(QWidget):
|
||||
|
||||
class ClientSubmissionFormWidget(SubmissionFormWidget):
|
||||
|
||||
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
|
||||
super().__init__(parent, submission=submission, disable=disable)
|
||||
self.disabler.setHidden(True)
|
||||
def __init__(self, parent: QWidget, clientsubmission: PydClientSubmission, samples: List = [],
|
||||
disable: list | None = None) -> None:
|
||||
super().__init__(parent, pyd=clientsubmission, disable=disable)
|
||||
try:
|
||||
self.disabler.setHidden(True)
|
||||
except AttributeError:
|
||||
pass
|
||||
# save_btn = QPushButton("Save")
|
||||
self.samples = samples
|
||||
logger.debug(f"Samples: {self.samples}")
|
||||
start_run_btn = QPushButton("Save")
|
||||
# self.layout.addWidget(save_btn)
|
||||
self.layout.addWidget(start_run_btn)
|
||||
start_run_btn.clicked.connect(self.create_new_submission)
|
||||
|
||||
|
||||
@report_result
|
||||
def parse_form(self) -> Report:
|
||||
"""
|
||||
Transforms form info into PydSubmission
|
||||
@@ -801,7 +816,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
|
||||
Report: Report on status of parse.
|
||||
"""
|
||||
report = Report()
|
||||
logger.info(f"Hello from client run form parser!")
|
||||
logger.info(f"Hello from client procedure form parser!")
|
||||
info = {}
|
||||
reagents = []
|
||||
for widget in self.findChildren(QWidget):
|
||||
@@ -827,18 +842,20 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
|
||||
report.add_result(report)
|
||||
return report
|
||||
|
||||
@report_result
|
||||
# @report_result
|
||||
def to_pydantic(self, *args):
|
||||
self.parse_form()
|
||||
return self.pyd
|
||||
|
||||
@report_result
|
||||
def create_new_submission(self, *args) -> Report:
|
||||
self.parse_form()
|
||||
sql = self.pyd.to_sql()
|
||||
pyd = self.to_pydantic()
|
||||
sql = pyd.to_sql()
|
||||
for sample in self.samples:
|
||||
if isinstance(sample, PydSample):
|
||||
sample = sample.to_sql()
|
||||
sql.add_sample(sample=sample)
|
||||
logger.debug(sql.__dict__)
|
||||
sql.save()
|
||||
self.app.table_widget.sub_wid.set_data()
|
||||
self.setParent(None)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Pane to hold information e.g. cost summary.
|
||||
"""
|
||||
from .info_tab import InfoPane
|
||||
from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
|
||||
from backend.db import Organization
|
||||
from backend.db import ClientLab
|
||||
from backend.excel import ReportMaker
|
||||
from .misc import CheckableComboBox
|
||||
import logging
|
||||
@@ -24,7 +24,7 @@ class Summary(InfoPane):
|
||||
self.org_select = CheckableComboBox()
|
||||
self.org_select.setEditable(False)
|
||||
self.org_select.addItem("Select", header=True)
|
||||
for org in [org.name for org in Organization.query()]:
|
||||
for org in [org.name for org in ClientLab.query()]:
|
||||
self.org_select.addItem(org)
|
||||
self.org_select.model().itemChanged.connect(self.update_data)
|
||||
self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1)
|
||||
|
||||
Reference in New Issue
Block a user