Frontend code cleanup finished.

This commit is contained in:
lwark
2025-09-05 15:23:56 -05:00
parent 610859d84f
commit c9396d6c41
25 changed files with 154 additions and 460 deletions

View File

@@ -3,4 +3,4 @@ Contains database, validators and excel operations.
""" """
from .db import * from .db import *
from .excel import * from .excel import *
from .validators import * from .validators import *

View File

@@ -484,7 +484,8 @@ class BaseClass(Base):
if check: if check:
try: try:
value = json.dumps(value) value = json.dumps(value)
except TypeError: except TypeError as e:
logger.error(f"Error json dumping value: {e}")
value = str(value) value = str(value)
try: try:
self._misc_info.update({key: value}) self._misc_info.update({key: value})

View File

@@ -917,7 +917,7 @@ class Procedure(BaseClass):
obj (_type_): parent widget obj (_type_): parent widget
""" """
logger.info(f"Add equipment") logger.info(f"Add equipment")
from frontend.widgets.equipment_usage_2 import EquipmentUsage from frontend.widgets.equipment_usage import EquipmentUsage
dlg = EquipmentUsage(parent=obj, procedure=self.to_pydantic()) dlg = EquipmentUsage(parent=obj, procedure=self.to_pydantic())
if dlg.exec(): if dlg.exec():
dlg.save_procedure() dlg.save_procedure()

View File

@@ -646,7 +646,7 @@ class Run(BaseClass, LogMixin):
'permission', "clientsubmission"] 'permission', "clientsubmission"]
output['sample_count'] = self.sample_count output['sample_count'] = self.sample_count
output['clientsubmission'] = self.clientsubmission.name output['clientsubmission'] = self.clientsubmission.name
output['clientlab'] = self.clientsubmission.clientlab # output['clientlab'] = self.clientsubmission.clientlab
output['started_date'] = self.started_date output['started_date'] = self.started_date
output['completed_date'] = self.completed_date output['completed_date'] = self.completed_date
return output return output

View File

@@ -126,7 +126,7 @@ class DefaultTABLEParser(DefaultParser):
df = df.dropna(axis=1, how='all') df = df.dropna(axis=1, how='all')
for ii, row in enumerate(df.iterrows()): for ii, row in enumerate(df.iterrows()):
output = {} output = {}
for key, value in row[1].details_dict().items(): for key, value in row[1].to_dict().items():
if isinstance(key, str): if isinstance(key, str):
key = key.lower().replace(" ", "_") key = key.lower().replace(" ", "_")
key = re.sub(r"_(\(.*\)|#)", "", key) key = re.sub(r"_(\(.*\)|#)", "", key)

View File

@@ -265,5 +265,5 @@ class RSLNamer(object):
return "" return ""
from .pydant import PydRun, PydKitType, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, \ from .pydant import PydRun, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, \
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults

View File

@@ -1497,9 +1497,11 @@ class PydClientSubmission(PydBaseClass):
@field_validator("sample_count") @field_validator("sample_count")
@classmethod @classmethod
def enforce_integer(cls, value): def enforce_integer(cls, value):
if not value['value']:
value['value'] = 0
try: try:
value['value'] = int(value['value']) value['value'] = int(value['value'])
except ValueError: except (ValueError, TypeError):
raise f"sample count value must be an integer" raise f"sample count value must be an integer"
return value return value

View File

@@ -1,10 +1,9 @@
''' """
Contains all operations for creating charts, graphs and visual effects. Contains all operations for creating charts, graphs and visual effects.
''' """
from datetime import timedelta, date from datetime import timedelta, date
from pathlib import Path from pathlib import Path
from typing import Generator from typing import Generator
import plotly import plotly
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import pandas as pd, logging import pandas as pd, logging
@@ -128,13 +127,10 @@ class CustomFigure(Figure):
html = f'<html><body>' html = f'<html><body>'
if self is not None: if self is not None:
# NOTE: Just cannot get this load from string to freaking work. # NOTE: Just cannot get this load from string to freaking work.
# html += self.to_html(include_plotlyjs='cdn', full_html=False)
html += plotly.offline.plot(self, output_type='div', include_plotlyjs="cdn") html += plotly.offline.plot(self, output_type='div', include_plotlyjs="cdn")
else: else:
html += "<h1>No data was retrieved for the given parameters.</h1>" html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>' html += '</body></html>'
# with open("test.html", "w", encoding="utf-8") as f:
# f.write(html)
return html return html

View File

@@ -3,10 +3,9 @@ Construct BC control concentration charts
""" """
from pprint import pformat from pprint import pformat
from . import CustomFigure from . import CustomFigure
import plotly.express as px import logging, sys, plotly.express as px
import pandas as pd import pandas as pd
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import logging
from operator import itemgetter from operator import itemgetter
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -31,7 +30,6 @@ class ConcentrationsChart(CustomFigure):
self.df = self.df.sort_values(['submitted_date', 'procedure'], ascending=[True, True]).reset_index( self.df = self.df.sort_values(['submitted_date', 'procedure'], ascending=[True, True]).reset_index(
drop=True) drop=True)
self.df = self.df.reset_index().rename(columns={"index": "idx"}) self.df = self.df.reset_index().rename(columns={"index": "idx"})
# logger.debug(f"DF after changes:\n{self.df}")
scatter = px.scatter(data_frame=self.df, x='procedure', y="concentration", scatter = px.scatter(data_frame=self.df, x='procedure', y="concentration",
hover_data=["name", "procedure", "submitted_date", "concentration"], hover_data=["name", "procedure", "submitted_date", "concentration"],
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"} color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}

View File

@@ -3,11 +3,9 @@ Functions for constructing irida control graphs using plotly.
""" """
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
import plotly.express as px import logging, plotly.express as px, pandas as pd
import pandas as pd
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from . import CustomFigure from . import CustomFigure
import logging
from tools import get_unique_values_in_df_column from tools import get_unique_values_in_df_column
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -22,9 +22,9 @@ from .date_type_picker import DateTypePicker
from .functions import select_save_file from .functions import select_save_file
from .pop_ups import HTMLPop from .pop_ups import HTMLPop
from .misc import Pagifier from .misc import Pagifier
from .submission_table import SubmissionsSheet, SubmissionsTree, ClientSubmissionRunModel from .submission_table import SubmissionsTree, ClientSubmissionRunModel
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer # from .controls_chart import ControlsViewer
from .summary import Summary from .summary import Summary
from .turnaround import TurnaroundTime from .turnaround import TurnaroundTime
from .concentrations import Concentrations from .concentrations import Concentrations
@@ -132,7 +132,7 @@ class App(QMainWindow):
self.table_widget.pager.current_page.textChanged.connect(self.update_data) self.table_widget.pager.current_page.textChanged.connect(self.update_data)
self.editReagentAction.triggered.connect(self.edit_reagent) self.editReagentAction.triggered.connect(self.edit_reagent)
self.manageOrgsAction.triggered.connect(self.manage_orgs) self.manageOrgsAction.triggered.connect(self.manage_orgs)
self.manageKitsAction.triggered.connect(self.manage_kits) # self.manageKitsAction.triggered.connect(self.manage_kits)
def showAbout(self): def showAbout(self):
""" """
@@ -195,24 +195,23 @@ class App(QMainWindow):
new_org = dlg.parse_form() new_org = dlg.parse_form()
new_org.save() new_org.save()
def manage_kits(self, *args, **kwargs): # def manage_kits(self, *args, **kwargs):
from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd # from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd
dlg = ManagerWindowPyd(parent=self, object_type=KitType, extras=[], add_edit='edit', managers=set()) # dlg = ManagerWindowPyd(parent=self, object_type=KitType, extras=[], add_edit='edit', managers=set())
if dlg.exec(): # if dlg.exec():
# logger.debug("\n\nBeginning parsing\n\n") # output = dlg.parse_form()
output = dlg.parse_form() # sql = output.to_sql()
# logger.debug(f"Kit output: {pformat(output.__dict__)}") # assert isinstance(sql, KitType)
# logger.debug("\n\nBeginning transformation\n\n") # sql.save()
sql = output.to_sql()
assert isinstance(sql, KitType)
sql.save()
@under_development @under_development
def submissions_to_excel(self, *args, **kwargs): def submissions_to_excel(self, *args, **kwargs):
from backend.db.models import Run
dlg = DateTypePicker(self) dlg = DateTypePicker(self)
if dlg.exec(): if dlg.exec():
output = dlg.parse_form() output = dlg.parse_form()
df = BasicRun.archive_submissions(**output) # TODO: Move to ClientSubmissions
df = Run.archive_submissions(**output)
filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx") filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx")
writer = ExcelWriter(filepath, "openpyxl") writer = ExcelWriter(filepath, "openpyxl")
df.to_excel(writer) df.to_excel(writer)
@@ -254,7 +253,6 @@ class AddSubForm(QWidget):
self.sheetwidget = QWidget(self) self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self) self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout) self.sheetwidget.setLayout(self.sheetlayout)
# self.sub_wid = SubmissionsSheet(parent=parent)
self.sub_wid = SubmissionsTree(parent=parent, model=ClientSubmissionRunModel(self)) self.sub_wid = SubmissionsTree(parent=parent, model=ClientSubmissionRunModel(self))
self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size) self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size)
self.sheetlayout.addWidget(self.sub_wid) self.sheetlayout.addWidget(self.sub_wid)
@@ -265,12 +263,10 @@ class AddSubForm(QWidget):
self.tab1.layout.addWidget(self.interior) self.tab1.layout.addWidget(self.interior)
self.tab1.layout.addWidget(self.sheetwidget) self.tab1.layout.addWidget(self.sheetwidget)
self.tab2.layout = QVBoxLayout(self) self.tab2.layout = QVBoxLayout(self)
# self.irida_viewer = ControlsViewer(self, archetype="Irida Control")
self.irida_viewer = None self.irida_viewer = None
self.tab2.layout.addWidget(self.irida_viewer) self.tab2.layout.addWidget(self.irida_viewer)
self.tab2.setLayout(self.tab2.layout) self.tab2.setLayout(self.tab2.layout)
self.tab3.layout = QVBoxLayout(self) self.tab3.layout = QVBoxLayout(self)
# self.pcr_viewer = ControlsViewer(self, archetype="PCR Control")
self.pcr_viewer = None self.pcr_viewer = None
self.tab3.layout.addWidget(self.pcr_viewer) self.tab3.layout.addWidget(self.pcr_viewer)
self.tab3.setLayout(self.tab3.layout) self.tab3.setLayout(self.tab3.layout)

View File

@@ -43,10 +43,8 @@ class Concentrations(InfoPane):
None None
""" """
include = self.pos_neg.get_checked() include = self.pos_neg.get_checked()
# logger.debug(f"Include: {include}")
super().update_data() super().update_data()
months = self.diff_month(self.start_date, self.end_date) months = self.diff_month(self.start_date, self.end_date)
# logger.debug(f"Box checked: {self.all_box.isChecked()}")
chart_settings = dict(start_date=self.start_date, end_date=self.end_date, chart_settings = dict(start_date=self.start_date, end_date=self.end_date,
include=include) include=include)
self.report_obj = ConcentrationMaker(**chart_settings) self.report_obj = ConcentrationMaker(**chart_settings)

View File

@@ -108,7 +108,6 @@ class ControlsViewer(InfoPane):
parent=self, parent=self,
months=months months=months
) )
# logger.debug(f"Chart settings: {chart_settings}")
self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx) self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name) self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name)
if issubclass(self.fig.__class__, CustomFigure): if issubclass(self.fig.__class__, CustomFigure):

View File

@@ -1,8 +1,14 @@
"""
"""
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QDialogButtonBox QVBoxLayout, QDialog, QDialogButtonBox
) )
from .misc import CheckableComboBox, StartEndDatePicker from .misc import CheckableComboBox, StartEndDatePicker
from backend.db.models.procedures import SubmissionType from backend.db.models.procedures import SubmissionType
import logging
logger = logging.getLogger(f"submissions.{__name__}")
class DateTypePicker(QDialog): class DateTypePicker(QDialog):
@@ -27,10 +33,7 @@ class DateTypePicker(QDialog):
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self): def parse_form(self):
# sub_types = [self.typepicker.itemText(i) for i in range(self.typepicker.count()) if self.typepicker.itemChecked(i)]
sub_types = self.typepicker.get_checked() sub_types = self.typepicker.get_checked()
start_date = self.datepicker.start_date.date().toPyDate() start_date = self.datepicker.start_date.date().toPyDate()
end_date = self.datepicker.end_date.date().toPyDate() end_date = self.datepicker.end_date.date().toPyDate()
return dict(submissiontype=sub_types, start_date=start_date, end_date=end_date) return dict(submissiontype=sub_types, start_date=start_date, end_date=end_date)

View File

@@ -1,91 +1,97 @@
''' """
Creates forms that the user can enter equipment info into. Creates forms that the user can enter equipment info into.
''' """
import sys, logging
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
)
from backend.db.models import Equipment, Run, Process, Procedure
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging
from typing import Generator from typing import Generator
from PyQt6.QtCore import Qt, pyqtSlot, QSignalBlocker
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QDialogButtonBox, QGridLayout, QWidget, QCheckBox, QComboBox, QLabel
)
from backend import Process
from backend.db.models import Equipment
from backend.validators.pydant import PydProcedure, PydEquipmentRole, PydTips, PydEquipment
from tools import get_application_from_parent, render_details_template
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog): class EquipmentUsage(QDialog):
def __init__(self, parent, procedure: Procedure): def __init__(self, parent, procedure: PydProcedure):
super().__init__(parent) super().__init__(parent)
self.procedure = procedure self.procedure = procedure
self.setWindowTitle(f"Equipment Checklist - {procedure.name}") self.setWindowTitle(f"Equipment Checklist - {procedure.name}")
self.used_equipment = self.procedure.equipment self.used_equipment = self.procedure.equipment
# self.kit = self.procedure.kittype self.kit = self.procedure.kittype
self.opt_equipment = procedure.proceduretype.get_equipment() self.opt_equipment = procedure.proceduretype.get_equipment()
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self)
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.webview.setMinimumSize(1200, 800)
self.webview.setMaximumWidth(1200)
# NOTE: Decide if exporting should be allowed.
self.layout = QGridLayout()
# NOTE: button to export a pdf version
self.layout.addWidget(self.webview, 1, 0, 10, 10)
self.setLayout(self.layout) self.setLayout(self.layout)
self.populate_form() self.setFixedWidth(self.webview.width() + 20)
# NOTE: setup channel
def populate_form(self): self.channel = QWebChannel()
""" self.channel.registerObject('backend', self)
Create form widgets html = self.construct_html(procedure=procedure)
""" self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
label = self.LabelRow(parent=self) self.layout.addWidget(self.buttonBox, 11, 1, 1, 1)
self.layout.addWidget(label)
for equipment in self.opt_equipment:
widg = equipment.to_form(parent=self, used=self.used_equipment)
self.layout.addWidget(widg)
widg.update_processes()
self.layout.addWidget(self.buttonBox)
def parse_form(self) -> Generator[PydEquipment, None, None]: @classmethod
""" def construct_html(cls, procedure: PydProcedure, child: bool = False):
Pull info from all RoleComboBox widgets proceduretype = procedure.proceduretype
proceduretype_dict = proceduretype.details_dict()
run = procedure.run
html = render_details_template(
template_name="support/equipment_usage",
css_in=[],
js_in=[],
proceduretype=proceduretype_dict,
run=run.details_dict(),
procedure=procedure.__dict__,
child=child
)
return html
Returns: @pyqtSlot(str, str, str, str)
Generator[PydEquipment, None, None]: All equipment pulled from widgets def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
""" try:
for widget in self.findChildren(QWidget): equipment_of_interest = next(
match widget: (item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
case RoleComboBox(): except StopIteration:
if widget.check.isChecked(): equipment_of_interest = None
item = widget.parse_form() equipment = Equipment.query(name=equipment)
if item: if equipment_of_interest:
yield item eoi = self.procedure.equipment.pop(self.procedure.equipment.index(equipment_of_interest))
else: else:
continue eoi = equipment.to_pydantic(proceduretype=self.procedure.proceduretype)
else: eoi.name = equipment.name
continue eoi.asset_number = equipment.asset_number
case _: eoi.nickname = equipment.nickname
continue process = next((prcss for prcss in equipment.process if prcss.name == process))
eoi.process = process.to_pydantic()
tips = next((tps for tps in equipment.tips if tps.name == tips))
eoi.tips = tips.to_pydantic()
self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment: {self.procedure.equipment}")
class LabelRow(QWidget): def save_procedure(self):
"""Provides column headers""" sql, _ = self.procedure.to_sql()
sql.save()
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QGridLayout()
self.check = QCheckBox()
self.layout.addWidget(self.check, 0, 0)
self.check.stateChanged.connect(self.check_all)
for iii, item in enumerate(["Role", "Equipment", "Process", "Tips"], start=1):
label = QLabel(item)
label.setMaximumWidth(200)
label.setMinimumWidth(200)
self.layout.addWidget(label, 0, iii, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout)
def check_all(self):
"""
Toggles all checkboxes in the form
"""
for object in self.parent().findChildren(QCheckBox):
object.setChecked(self.check.isChecked())
class RoleComboBox(QWidget): class RoleComboBox(QWidget):
@@ -124,7 +130,6 @@ class RoleComboBox(QWidget):
""" """
equip = self.box.currentText() equip = self.box.currentText()
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0]) equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
logger.debug(f"Equip2: {equip2}")
with QSignalBlocker(self.process) as blocker: with QSignalBlocker(self.process) as blocker:
self.process.clear() self.process.clear()
self.process.addItems([item for item in equip2.process if item in self.role.process]) self.process.addItems([item for item in equip2.process if item in self.role.process])
@@ -180,7 +185,7 @@ class RoleComboBox(QWidget):
def toggle_checked(self): def toggle_checked(self):
""" """
If this equipment is disabled, the input fields will be disabled. If this equipment is disabled, the input fields will be disabled.
""" """
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):
match widget: match widget:
case QCheckBox(): case QCheckBox():

View File

@@ -1,131 +0,0 @@
'''
Creates forms that the user can enter equipment info into.
'''
import sys
from pprint import pformat
from PyQt6.QtCore import Qt, QSignalBlocker, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
)
from backend.db.models import Equipment, Run, Process, Procedure, Tips
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips, PydProcedure
import logging
from typing import Generator
from tools import get_application_from_parent, render_details_template, flatten_list
logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, procedure: PydProcedure):
super().__init__(parent)
self.procedure = procedure
self.setWindowTitle(f"Equipment Checklist - {procedure.name}")
self.used_equipment = self.procedure.equipment
self.kit = self.procedure.kittype
self.opt_equipment = procedure.proceduretype.get_equipment()
self.layout = QVBoxLayout()
self.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self)
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.webview.setMinimumSize(1200, 800)
self.webview.setMaximumWidth(1200)
# NOTE: Decide if exporting should be allowed.
# self.webview.loadFinished.connect(self.activate_export)
self.layout = QGridLayout()
# NOTE: button to export a pdf version
self.layout.addWidget(self.webview, 1, 0, 10, 10)
self.setLayout(self.layout)
self.setFixedWidth(self.webview.width() + 20)
# NOTE: setup channel
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
html = self.construct_html(procedure=procedure)
self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.buttonBox, 11, 1, 1, 1)
@classmethod
def construct_html(cls, procedure: PydProcedure, child: bool = False):
proceduretype = procedure.proceduretype
proceduretype_dict = proceduretype.details_dict()
run = procedure.run
# proceduretype_dict['equipment_json'] = flatten_list([item['equipment_json'] for item in proceduretype_dict['equipment']])
# proceduretype_dict['equipment_json'] = [
# {'name': 'Liquid Handler', 'equipment': [
# {'name': 'Other', 'asset_number': 'XXX', 'processes': [
# {'name': 'Trust Me', 'tips': ['Blah']},
# {'name': 'No Me', 'tips': ['Blah', 'Crane']}
# ]
# },
# {'name': 'Biomek', 'asset_number': '5015530', 'processes': [
# {'name': 'Sample Addition', 'tips': ['Axygen 20uL']
# }
# ]
# }
# ]
# }
# ]
# if procedure.equipment:
# for equipmentrole in proceduretype_dict['equipment']:
# # NOTE: Check if procedure equipment is present and move to head of the list if so.
# try:
# relevant_procedure_item = next((equipment for equipment in procedure.equipment if
# equipment.equipmentrole == equipmentrole['name']))
# except StopIteration:
# continue
# item_in_er_list = next((equipment for equipment in equipmentrole['equipment'] if
# equipment['name'] == relevant_procedure_item.name))
# equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
# equipmentrole['equipment'].index(item_in_er_list)))
html = render_details_template(
template_name="support/equipment_usage",
css_in=[],
js_in=[],
proceduretype=proceduretype_dict,
run=run.details_dict(),
procedure=procedure.__dict__,
child=child
)
return html
@pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
try:
equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
except StopIteration:
equipment_of_interest = None
equipment = Equipment.query(name=equipment)
if equipment_of_interest:
eoi = self.procedure.equipment.pop(self.procedure.equipment.index(equipment_of_interest))
else:
eoi = equipment.to_pydantic(proceduretype=self.procedure.proceduretype)
eoi.name = equipment.name
eoi.asset_number = equipment.asset_number
eoi.nickname = equipment.nickname
process = next((prcss for prcss in equipment.process if prcss.name == process))
eoi.process = process.to_pydantic()
tips = next((tps for tps in equipment.tips if tps.name == tips))
eoi.tips = tips.to_pydantic()
self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment: {self.procedure.equipment}")
def save_procedure(self):
sql, _ = self.procedure.to_sql()
logger.debug(pformat(sql.__dict__))
# import pickle
# with open("sql.pickle", "wb") as f:
# pickle.dump(sql, f)
# with open("pyd.pickle", "wb") as f:
# pickle.dump(self.procedure, f)
sql.save()

View File

@@ -39,7 +39,7 @@ def select_open_file(obj: QMainWindow, file_extension: str | None = None) -> Pat
logger.warning(f"No file selected, cancelling.") logger.warning(f"No file selected, cancelling.")
return return
obj.last_dir = fname.parent obj.last_dir = fname.parent
logger.debug(f"File selected: {fname}") logger.info(f"File selected: {fname}")
return fname return fname

View File

@@ -5,10 +5,9 @@ from operator import itemgetter
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QDialog, QGridLayout, QLabel, QLineEdit, QDialogButtonBox, QTextEdit, QComboBox QWidget, QDialog, QGridLayout, QLabel, QLineEdit, QDialogButtonBox, QTextEdit, QComboBox
) )
import pyqtgraph as pg
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon
from PIL import Image from PIL import Image
import logging, numpy as np import logging, numpy as np, pyqtgraph as pg
from pprint import pformat from pprint import pformat
from typing import Tuple, List from typing import Tuple, List
from pathlib import Path from pathlib import Path

View File

@@ -34,7 +34,6 @@ class InfoPane(QWidget):
report = Report() report = Report()
self.start_date = self.datepicker.start_date.date().toPyDate() self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate() self.end_date = self.datepicker.end_date.date().toPyDate()
# logger.debug(f"Start date: {self.start_date}, End date: {self.end_date}")
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): if self.datepicker.start_date.date() > self.datepicker.end_date.date():
lastmonth = self.datepicker.end_date.date().addDays(-31) lastmonth = self.datepicker.end_date.date().addDays(-31)
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}." msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."

View File

@@ -1,7 +1,7 @@
""" """
Contains miscellaneous widgets for frontend functions Contains miscellaneous widgets for frontend functions
""" """
import math import math, logging
from PyQt6.QtGui import QStandardItem, QIcon from PyQt6.QtGui import QStandardItem, QIcon
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QLineEdit, QComboBox, QDateEdit, QPushButton, QWidget, QLabel, QLineEdit, QComboBox, QDateEdit, QPushButton, QWidget,
@@ -10,7 +10,6 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import Qt, QDate, QSize from PyQt6.QtCore import Qt, QDate, QSize
from tools import jinja_template_loading from tools import jinja_template_loading
from backend.db.models import * from backend.db.models import *
import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -8,7 +8,7 @@ from PyQt6.QtCore import pyqtSlot, Qt
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QGridLayout, QDialogButtonBox from PyQt6.QtWidgets import QDialog, QGridLayout, QDialogButtonBox
from typing import TYPE_CHECKING, Any, List from typing import TYPE_CHECKING, List
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.validators import PydProcedure, PydEquipment from backend.validators import PydProcedure, PydEquipment
from tools import get_application_from_parent, render_details_template, sanitize_object_for_json from tools import get_application_from_parent, render_details_template, sanitize_object_for_json
@@ -52,7 +52,7 @@ class ProcedureCreation(QDialog):
def set_html(self): def set_html(self):
from .equipment_usage_2 import EquipmentUsage from .equipment_usage import EquipmentUsage
proceduretype_dict = self.proceduretype.details_dict() proceduretype_dict = self.proceduretype.details_dict()
# NOTE: Add --New-- as an option for reagents. # NOTE: Add --New-- as an option for reagents.
for key, value in self.procedure.reagentrole.items(): for key, value in self.procedure.reagentrole.items():
@@ -88,11 +88,6 @@ class ProcedureCreation(QDialog):
@pyqtSlot(str, str, str, str) @pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str): def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
from backend.db.models import Equipment, ProcessVersion, TipsLot from backend.db.models import Equipment, ProcessVersion, TipsLot
logger.debug(f"Updating equipment with"
f"\n\tEquipment role: {equipmentrole}"
f"\n\tEquipment: {equipment}"
f"\n\tProcess: {process}"
f"\n\tTips: {tips}")
try: try:
equipment_of_interest = next( equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole)) (item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
@@ -109,19 +104,13 @@ class ProcedureCreation(QDialog):
process_name, version = process.split("-v") process_name, version = process.split("-v")
process = ProcessVersion.query(name=process_name, version=version, limit=1) process = ProcessVersion.query(name=process_name, version=version, limit=1)
eoi.process = process eoi.process = process
# sys.exit(f"Process:\n{pformat(eoi.process.__dict__)}")
try: try:
tips_manufacturer, tipsref, lot = [item if item != "" else None for item in tips.split("-")] tips_manufacturer, tipsref, lot = [item if item != "" else None for item in tips.split("-")]
logger.debug(f"Querying with '{tips_manufacturer}', '{tipsref}', '{lot}'")
tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot) tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot)
logger.debug(f"Found tips: {tips}")
eoi.tips = tips eoi.tips = tips
except ValueError: except ValueError:
logger.warning(f"No tips info to unpack") logger.warning(f"No tips info to unpack")
# tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot)
# eoi.tips = tips
self.procedure.equipment.append(eoi) self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment:\n{pformat([item.__dict__ for item in self.procedure.equipment])}")
@pyqtSlot(str, str) @pyqtSlot(str, str)
def text_changed(self, key: str, new_value: str): def text_changed(self, key: str, new_value: str):
@@ -167,11 +156,9 @@ class ProcedureCreation(QDialog):
@pyqtSlot(str, str) @pyqtSlot(str, str)
def update_reagent(self, reagentrole: str, name_lot_expiry: str): def update_reagent(self, reagentrole: str, name_lot_expiry: str):
logger.debug(f"{reagentrole}: {name_lot_expiry}")
try: try:
name, lot, expiry = name_lot_expiry.split(" - ") name, lot, expiry = name_lot_expiry.split(" - ")
except ValueError as e: except ValueError as e:
logger.debug(f"Couldn't perform split due to {e}")
return return
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)

View File

@@ -1,5 +1,7 @@
"""
"""
import logging import logging
from pathlib import Path
from typing import List from typing import List
from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
@@ -22,7 +24,6 @@ class SampleChecker(QDialog):
self.rsl_plate_number = RSLNamer.construct_new_plate_name(clientsubmission.to_dict()) self.rsl_plate_number = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
else: else:
self.rsl_plate_number = clientsubmission self.rsl_plate_number = clientsubmission
logger.debug(f"RSL Plate number: {self.rsl_plate_number}")
self.samples = samples self.samples = samples
self.setWindowTitle(title) self.setWindowTitle(title)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -35,16 +36,11 @@ class SampleChecker(QDialog):
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject('backend', self) self.channel.registerObject('backend', self)
# NOTE: Used to maintain javascript functions. # NOTE: Used to maintain javascript functions.
# template = env.get_template("sample_checker.html")
# template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
# with open(template_path.joinpath("css", "styles.css"), "r") as f:
# css = [f.read()]
try: try:
samples = self.formatted_list samples = self.formatted_list
except AttributeError as e: except AttributeError as e:
logger.error(f"Problem getting sample list: {e}") logger.error(f"Problem getting sample list: {e}")
samples = [] samples = []
# html = template.render(samples=samples, css=css, rsl_plate_number=self.rsl_plate_number)
html = render_details_template(template_name="sample_checker", samples=samples, rsl_plate_number=self.rsl_plate_number) html = render_details_template(template_name="sample_checker", samples=samples, rsl_plate_number=self.rsl_plate_number)
self.webview.setHtml(html) self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel) self.webview.page().setWebChannel(self.channel)
@@ -55,13 +51,8 @@ class SampleChecker(QDialog):
self.layout.addWidget(self.buttonBox, 11, 9, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) self.layout.addWidget(self.buttonBox, 11, 9, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout) self.setLayout(self.layout)
# with open("sample_checker_rendered.html", "w") as f:
# f.write(html)
logger.debug(f"HTML sample checker written!")
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
def text_changed(self, submission_rank: str, key: str, new_value: str): def text_changed(self, submission_rank: str, key: str, new_value: str):
logger.debug(f"Name: {submission_rank}, Key: {key}, Value: {new_value}")
try: try:
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank)) item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
except StopIteration: except StopIteration:
@@ -71,7 +62,6 @@ class SampleChecker(QDialog):
@pyqtSlot(int, bool) @pyqtSlot(int, bool)
def enable_sample(self, submission_rank: int, enabled: bool): def enable_sample(self, submission_rank: int, enabled: bool):
logger.debug(f"Name: {submission_rank}, Enabled: {enabled}")
try: try:
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank)) item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
except StopIteration: except StopIteration:
@@ -81,14 +71,12 @@ class SampleChecker(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def set_rsl_plate_number(self, rsl_plate_number: str): def set_rsl_plate_number(self, rsl_plate_number: str):
logger.debug(f"RSL plate num: {rsl_plate_number}")
self.rsl_plate_number = rsl_plate_number self.rsl_plate_number = rsl_plate_number
@property @property
def formatted_list(self) -> List[dict]: def formatted_list(self) -> List[dict]:
output = [] output = []
for sample in self.samples: for sample in self.samples:
# logger.debug(sample)
s = sample.improved_dict(dictionaries=False) s = sample.improved_dict(dictionaries=False)
if s['sample_id'] in [item['sample_id'] for item in output]: if s['sample_id'] in [item['sample_id'] for item in output]:
s['color'] = "red" s['color'] = "red"

View File

@@ -62,14 +62,9 @@ class SubmissionDetails(QDialog):
css = f.read() css = f.read()
key = object.__class__.__name__.lower() key = object.__class__.__name__.lower()
d = {key: details} d = {key: details}
# logger.debug(f"Using details: {pformat(d['procedure']['equipment'])}")
html = template.render(**d, css=[css]) html = template.render(**d, css=[css])
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}") self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}")
# with open(f"{object.__class__.__name__}_details_rendered.html", "w") as f:
# f.write(html)
# pass
def activate_export(self) -> None: def activate_export(self) -> None:
""" """
@@ -96,10 +91,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def equipment_details(self, equipment: str | Equipment): def equipment_details(self, equipment: str | Equipment):
logger.debug(f"Equipment details")
if isinstance(equipment, str): if isinstance(equipment, str):
equipment = Equipment.query(name=equipment) equipment = Equipment.query(name=equipment)
base_dict = equipment.to_sub_dict(full_data=True) # base_dict = equipment.to_sub_dict(full_data=True)
base_dict = equipment.details_dict()
template = equipment.details_template template = equipment.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
@@ -110,10 +105,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def process_details(self, process: str | Process): def process_details(self, process: str | Process):
logger.debug(f"Process details")
if isinstance(process, str): if isinstance(process, str):
process = Process.query(name=process) process = Process.query(name=process)
base_dict = process.to_sub_dict(full_data=True) # base_dict = process.to_sub_dict(full_data=True)
base_dict = process.details_dict()
template = process.details_template template = process.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
@@ -124,10 +119,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def tips_details(self, tips: str | Tips): def tips_details(self, tips: str | Tips):
logger.debug(f"Equipment details: {tips}")
if isinstance(tips, str): if isinstance(tips, str):
tips = Tips.query(lot=tips) tips = Tips.query(lot=tips)
base_dict = tips.to_sub_dict(full_data=True) # base_dict = tips.to_sub_dict(full_data=True)
base_dict = tips.details_dict()
template = tips.details_template template = tips.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
@@ -144,10 +139,10 @@ class SubmissionDetails(QDialog):
Args: Args:
sample (str): Submitter Id of the sample. sample (str): Submitter Id of the sample.
""" """
logger.debug(f"Sample details.")
if isinstance(sample, str): if isinstance(sample, str):
sample = Sample.query(sample_id=sample) sample = Sample.query(sample_id=sample)
base_dict = sample.to_sub_dict(full_data=True) # base_dict = sample.to_sub_dict(full_data=True)
base_dict = sample.details_dict()
exclude = ['procedure', 'excluded', 'colour', 'tooltip'] exclude = ['procedure', 'excluded', 'colour', 'tooltip']
base_dict['excluded'] = exclude base_dict['excluded'] = exclude
template = sample.details_template template = sample.details_template
@@ -155,8 +150,6 @@ class SubmissionDetails(QDialog):
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
html = template.render(sample=base_dict, css=css) html = template.render(sample=base_dict, css=css)
# with open(f"{sample.sample_id}.html", 'w') as f:
# f.write(html)
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"Sample Details - {sample.sample_id}") self.setWindowTitle(f"Sample Details - {sample.sample_id}")
@@ -169,13 +162,13 @@ class SubmissionDetails(QDialog):
kit (str | KitType): Name of kittype. kit (str | KitType): Name of kittype.
reagent (str | Reagent): Lot number of the reagent reagent (str | Reagent): Lot number of the reagent
""" """
logger.debug(f"Reagent details.")
if isinstance(reagent, str): if isinstance(reagent, str):
reagent = Reagent.query(lot=reagent) reagent = Reagent.query(lot=reagent)
if isinstance(proceduretype, str): if isinstance(proceduretype, str):
self.proceduretype = ProcedureType.query(name=proceduretype) self.proceduretype = ProcedureType.query(name=proceduretype)
base_dict = reagent.to_sub_dict(proceduretype=self.proceduretype, full_data=True) # base_dict = reagent.to_sub_dict(proceduretype=self.proceduretype, full_data=True)
# base_dict = reagent.details_dict(proceduretype=self.proceduretype, full_data=True) # base_dict = reagent.details_dict(proceduretype=self.proceduretype, full_data=True)
base_dict = reagent.details_dict()
env = jinja_template_loading() env = jinja_template_loading()
temp_name = "reagent_details.html" temp_name = "reagent_details.html"
try: try:
@@ -221,7 +214,6 @@ class SubmissionDetails(QDialog):
Args: Args:
run (str | BasicRun): Submission of interest. run (str | BasicRun): Submission of interest.
""" """
logger.debug(f"Run details.")
if isinstance(run, str): if isinstance(run, str):
run = Run.query(name=run) run = Run.query(name=run)
self.rsl_plate_number = run.rsl_plate_number self.rsl_plate_number = run.rsl_plate_number
@@ -234,7 +226,6 @@ class SubmissionDetails(QDialog):
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
# 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.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html) self.webview.setHtml(self.html)

View File

@@ -4,13 +4,11 @@ Contains widgets specific to the procedure summary and procedure details.
import sys, logging, re import sys, logging, re
from pprint import pformat from pprint import pformat
from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \ from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent
from typing import Dict, List from typing import Dict, List
# from backend import Procedure # from backend import Procedure
from backend.db.models.submissions import Run, ClientSubmission from backend.db.models.submissions import Run, ClientSubmission
from backend.db.models.procedures import Procedure from backend.db.models.procedures import Procedure
@@ -91,7 +89,6 @@ class SubmissionsSheet(QTableView):
""" """
sets data in model sets data in model
""" """
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = Run.submissions_to_df(page=page, page_size=page_size) self.data = Run.submissions_to_df(page=page, page_size=page_size)
try: try:
self.data['Id'] = self.data['Id'].apply(str) self.data['Id'] = self.data['Id'].apply(str)
@@ -232,29 +229,6 @@ class SubmissionsSheet(QTableView):
return report return report
# class ClientSubmissionDelegate(QStyledItemDelegate):
#
# def __init__(self, parent=None):
# super(ClientSubmissionDelegate, self).__init__(parent)
# pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
# icon1 = QWidget().style().standardIcon(pixmapi)
# pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
# icon2 = QWidget().style().standardIcon(pixmapi)
# self._plus_icon = icon1
# self._minus_icon = icon2
#
# def initStyleOption(self, 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): class SubmissionsTree(QTreeView):
""" """
https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt
@@ -264,20 +238,12 @@ class SubmissionsTree(QTreeView):
super(SubmissionsTree, self).__init__(parent) super(SubmissionsTree, self).__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count() self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
# self.setIndentation(0)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
# self.clicked.connect(self.on_clicked)
# delegate1 = ClientSubmissionDelegate(self)
# self.setItemDelegateForColumn(0, delegate1)
self.model = model self.model = model
self.setModel(self.model) self.setModel(self.model)
# self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents)
self.setSelectionBehavior(QAbstractItemView.selectionBehavior(self).SelectRows) self.setSelectionBehavior(QAbstractItemView.selectionBehavior(self).SelectRows)
# self.setStyleSheet("background-color: #0D1225;")
self.set_data() self.set_data()
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
# self.customContextMenuRequested.connect(self.open_menu)
self.setStyleSheet(""" self.setStyleSheet("""
QTreeView { QTreeView {
background-color: #f5f5f5; background-color: #f5f5f5;
@@ -294,20 +260,13 @@ class SubmissionsTree(QTreeView):
} }
""") """)
# Enable alternating row colors # Note: Enable alternating row colors
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setIndentation(20) self.setIndentation(20)
self.setItemsExpandable(True) self.setItemsExpandable(True)
# self.expanded.connect(self.expand_item)
for ii in range(2): for ii in range(2):
self.resizeColumnToContents(ii) 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 expand_item(self, event: QModelIndex): def expand_item(self, event: QModelIndex):
logger.debug(f"Data: {event.data()}") logger.debug(f"Data: {event.data()}")
logger.debug(f"Parent {event.parent().data()}") logger.debug(f"Parent {event.parent().data()}")
@@ -327,18 +286,11 @@ class SubmissionsTree(QTreeView):
""" """
indexes = self.selectedIndexes() indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1))) dicto = next((item.data(1) for item in indexes if item.data(1)))
logger.debug(f"Dicto: {pformat(dicto)}")
query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(f"Querying: {query_obj}")
# NOTE: Convert to data in id column (i.e. column 0) # 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.menu = QMenu(self)
self.con_actions = query_obj.custom_context_events self.con_actions = query_obj.custom_context_events
logger.debug(f"Context menu actions: {self.con_actions}")
for key in self.con_actions.keys(): for key in self.con_actions.keys():
logger.debug(key)
match key.lower(): match key.lower():
case "add procedure": case "add procedure":
action = QMenu(self.menu) action = QMenu(self.menu)
@@ -362,7 +314,7 @@ class SubmissionsTree(QTreeView):
action = QAction(key, self) action = QAction(key, self)
action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self)) action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self))
self.menu.addAction(action) self.menu.addAction(action)
# # NOTE: add other required actions # NOTE: add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
def set_data(self, page: int = 1, page_size: int = 250) -> None: def set_data(self, page: int = 1, page_size: int = 250) -> None:
@@ -372,8 +324,6 @@ class SubmissionsTree(QTreeView):
self.clear() self.clear()
self.data = [item.to_dict(full_data=True) for item in self.data = [item.to_dict(full_data=True) for item in
ClientSubmission.query(chronologic=True, page=page, page_size=page_size)] ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
# logger.debug(f"setting data:\n {pformat(self.data)}")
# sys.exit()
root = self.model.invisibleRootItem() root = self.model.invisibleRootItem()
for submission in self.data: for submission in self.data:
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}" group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
@@ -382,26 +332,21 @@ class SubmissionsTree(QTreeView):
query_str=submission['submitter_plate_id'], query_str=submission['submitter_plate_id'],
item_type=ClientSubmission item_type=ClientSubmission
)) ))
# logger.debug(f"Added {submission_item}")
for run in submission['run']: for run in submission['run']:
# self.model.append_element_to_group(group_item=group_item, element=run)
run_item = self.model.add_child(parent=submission_item, child=dict( run_item = self.model.add_child(parent=submission_item, child=dict(
name=run['plate_number'], name=run['plate_number'],
query_str=run['plate_number'], query_str=run['plate_number'],
item_type=Run item_type=Run
)) ))
# logger.debug(f"Added {run_item}")
for procedure in run['procedures']: for procedure in run['procedures']:
procedure_item = self.model.add_child(parent=run_item, child=dict( procedure_item = self.model.add_child(parent=run_item, child=dict(
name=procedure['name'], name=procedure['name'],
query_str=procedure['name'], query_str=procedure['name'],
item_type=Procedure item_type=Procedure
)) ))
# logger.debug(f"Added {procedure_item}")
def _populateTree(self, children, parent): def _populateTree(self, children, parent):
for child in children: for child in children:
logger.debug(child)
child_item = QStandardItem(child['name']) child_item = QStandardItem(child['name'])
parent.appendRow(child_item) parent.appendRow(child_item)
if isinstance(children, List): if isinstance(children, List):
@@ -409,22 +354,13 @@ class SubmissionsTree(QTreeView):
def clear(self): def clear(self):
if self.model != None: if self.model != None:
# self.model.clear() # works
self.model.setRowCount(0) # works self.model.setRowCount(0) # works
def show_details(self, sel: QModelIndex): def show_details(self, sel: QModelIndex):
# id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0) # NOTE: Convert to data in id column (i.e. column 0)
# id = id.sibling(id.row(), 1)
indexes = self.selectedIndexes() indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1))) dicto = next((item.data(1) for item in indexes if item.data(1)))
# try:
# id = int(id.data())
# except ValueError:
# return
# Run.query(id=id).show_details(self)
obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(obj)
obj.show_details(self) obj.show_details(self)
def link_extractions(self): def link_extractions(self):
@@ -437,14 +373,9 @@ class SubmissionsTree(QTreeView):
class ClientSubmissionRunModel(QStandardItemModel): class ClientSubmissionRunModel(QStandardItemModel):
def __init__(self, parent=None): # def __init__(self, parent=None):
super(ClientSubmissionRunModel, self).__init__(parent) # super(ClientSubmissionRunModel, self).__init__(parent)
# headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"] #
# self.setColumnCount(len(headers))
# self.setHorizontalHeaderLabels(headers)
def add_child(self, parent: QStandardItem, child:dict): def add_child(self, parent: QStandardItem, child:dict):
item = QStandardItem(child['name']) item = QStandardItem(child['name'])
item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1) item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1)
@@ -453,4 +384,4 @@ class ClientSubmissionRunModel(QStandardItemModel):
return item return item
def edit_item(self): def edit_item(self):
pass pass

View File

@@ -1,15 +1,13 @@
""" """
Contains all procedure related frontend functions Contains all procedure related frontend functions
""" """
import sys import sys, logging
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout
) )
from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker
from .functions import select_open_file, select_save_file from .functions import select_open_file, select_save_file
import logging
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.validators import PydReagent, PydClientSubmission, PydSample from backend.validators import PydReagent, PydClientSubmission, PydSample
@@ -121,37 +119,16 @@ class SubmissionFormContainer(QWidget):
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical")) report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
return report return report
# NOTE: create sheetparser using excel sheet and context from gui # NOTE: create sheetparser using excel sheet and context from gui
# try:
# self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
# except PermissionError:
# logger.error(f"Couldn't get permission to access file: {fname}")
# return
# except AttributeError:
# self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
# try:
# # self.prsr = SheetParser(filepath=fname)
# self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
# except PermissionError:
# logger.error(f"Couldn't get permission to access file: {fname}")
# return
# except AttributeError:
# self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
# self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
# self.pydsamples = self.sampleparser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname) self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname)
self.pydclientsubmission = self.clientsubmission_manager.to_pydantic() self.pydclientsubmission = self.clientsubmission_manager.to_pydantic()
checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.sample) checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.sample)
if checker.exec(): if checker.exec():
# logger.debug(pformat(self.pydclientsubmission.sample))
try: try:
assert isinstance(self.pydclientsubmission, PydClientSubmission) assert isinstance(self.pydclientsubmission, PydClientSubmission)
except AssertionError as e: except AssertionError as e:
logger.error(f"Got wrong type for {self.pydclientsubmission}: {type(self.pydclientsubmission)}") logger.error(f"Got wrong type for {self.pydclientsubmission}: {type(self.pydclientsubmission)}")
raise e raise e
self.form = self.pydclientsubmission.to_form(parent=self) self.form = self.pydclientsubmission.to_form(parent=self)
# self.form.samples = self.pydsamples
self.layout().addWidget(self.form) self.layout().addWidget(self.form)
else: else:
message = "Submission cancelled." message = "Submission cancelled."
@@ -195,14 +172,11 @@ class SubmissionFormWidget(QWidget):
self.pyd = pyd self.pyd = pyd
self.missing_info = [] self.missing_info = []
self.submissiontype = SubmissionType.query(name=self.pyd.submissiontype['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']) defaults = Run.get_default_info("form_recover", "form_ignore", submissiontype=self.pyd.submissiontype['value'])
self.recover = defaults['form_recover'] self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore'] self.ignore = defaults['form_ignore']
self.layout = QVBoxLayout() 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: if k in self.ignore:
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget") logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
continue continue
@@ -218,7 +192,6 @@ class SubmissionFormWidget(QWidget):
value = self.pyd.model_extra[k] value = self.pyd.model_extra[k]
except KeyError: except KeyError:
value = dict(value=None, missing=True) value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}")
add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype, add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype,
run_object=Run(), disable=check) run_object=Run(), disable=check)
if add_widget is not None: if add_widget is not None:
@@ -230,7 +203,6 @@ class SubmissionFormWidget(QWidget):
self.layout.addWidget(self.disabler) self.layout.addWidget(self.disabler)
self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents) self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
self.setStyleSheet(main_form_style) self.setStyleSheet(main_form_style)
# self.scrape_reagents(self.kittype)
self.setLayout(self.layout) self.setLayout(self.layout)
def disable_reagents(self): def disable_reagents(self):
@@ -298,7 +270,6 @@ class SubmissionFormWidget(QWidget):
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton): if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
reagent.setParent(None) reagent.setParent(None)
reagents, integrity_report, missing_reagents = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit) reagents, integrity_report, missing_reagents = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit)
# logger.debug(f"Reagents: {reagents}")
expiry_report = self.pyd.check_reagent_expiries(exempt=missing_reagents) expiry_report = self.pyd.check_reagent_expiries(exempt=missing_reagents)
for reagent in reagents: for reagent in reagents:
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit) add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit)
@@ -364,34 +335,6 @@ class SubmissionFormWidget(QWidget):
return report return report
base_submission = self.pyd.to_sql() base_submission = self.pyd.to_sql()
# NOTE: check output message for issues # NOTE: check output message for issues
# try:
# trigger = result.results[-1]
# code = trigger.code
# except IndexError as e:
# logger.error(result.results)
# logger.error(f"Problem getting error code: {e}")
# code = 0
# match code:
# # NOTE: code 0: everything is fine.
# case 0:
# pass
# # NOTE: code 1: ask for overwrite
# case 1:
# dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_number}?", message=trigger.msg)
# if dlg.exec():
# # NOTE: Do not add duplicate reagents.
# pass
# else:
# self.app.ctx.database_session.rollback()
# report.add_result(Result(msg="Overwrite cancelled", status="Information"))
# return report
# # NOTE: code 2: No RSL plate number given
# case 2:
# report.add_result(result)
# return report
# case _:
# pass
# NOTE: add reagents to procedure object
if base_submission is None: if base_submission is None:
return return
for reagent in base_submission.reagents: for reagent in base_submission.reagents:
@@ -450,7 +393,6 @@ class SubmissionFormWidget(QWidget):
if field is not None: if field is not None:
info[field] = value info[field] = value
self.pyd.reagents = reagents self.pyd.reagents = reagents
# logger.debug(f"Reagents from form: {reagents}")
for item in self.recover: for item in self.recover:
if hasattr(self, item): if hasattr(self, item):
value = getattr(self, item) value = getattr(self, item)
@@ -558,29 +500,29 @@ class SubmissionFormWidget(QWidget):
# NOTE: set combobox values to lookedup values # NOTE: set combobox values to lookedup values
add_widget.addItems(labs) add_widget.addItems(labs)
add_widget.setToolTip("Select submitting lab.") add_widget.setToolTip("Select submitting lab.")
case 'kittype': # case 'kittype':
# NOTE: if extraction kittype not available, all other values fail # # NOTE: if extraction kittype not available, all other values fail
if not check_not_nan(value): # if not check_not_nan(value):
msg = AlertPop(message="Make sure to check your extraction kittype in the excel sheet!", # msg = AlertPop(message="Make sure to check your extraction kittype in the excel sheet!",
status="warning") # status="warning")
msg.exec() # msg.exec()
# NOTE: create combobox to hold looked up kits # # NOTE: create combobox to hold looked up kits
add_widget = MyQComboBox(scrollWidget=parent) # add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup existing kits by 'proceduretype' decided on by sheetparser # # NOTE: lookup existing kits by 'proceduretype' decided on by sheetparser
uses = [item.name for item in submission_type.kit_types] # uses = [item.name for item in submission_type.kit_types]
obj.uses = uses # obj.uses = uses
if check_not_nan(value): # if check_not_nan(value):
try: # try:
uses.insert(0, uses.pop(uses.index(value))) # uses.insert(0, uses.pop(uses.index(value)))
except ValueError: # except ValueError:
logger.warning(f"Couldn't find kittype 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 # obj.ext_kit = value
else: # else:
logger.error(f"Couldn't find {obj.prsr.sub['kittype']}") # logger.error(f"Couldn't find {obj.prsr.sub['kittype']}")
obj.ext_kit = uses[0] # obj.ext_kit = uses[0]
add_widget.addItems(uses) # add_widget.addItems(uses)
add_widget.setToolTip("Select extraction kittype.") # add_widget.setToolTip("Select extraction kittype.")
parent.extraction_kit = add_widget.currentText() # parent.extraction_kit = add_widget.currentText()
case 'submission_category': case 'submission_category':
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
categories = ['Diagnostic', "Surveillance", "Research"] categories = ['Diagnostic', "Surveillance", "Research"]
@@ -813,11 +755,8 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
self.disabler.setHidden(True) self.disabler.setHidden(True)
except AttributeError: except AttributeError:
pass pass
# save_btn = QPushButton("Save")
self.sample = samples self.sample = samples
logger.debug(f"Samples: {self.sample}")
start_run_btn = QPushButton("Save") start_run_btn = QPushButton("Save")
# self.layout.addWidget(save_btn)
self.layout.addWidget(start_run_btn) self.layout.addWidget(start_run_btn)
start_run_btn.clicked.connect(self.create_new_submission) start_run_btn.clicked.connect(self.create_new_submission)
@@ -846,7 +785,6 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
field, value = widget.parse_form() field, value = widget.parse_form()
if field is not None: if field is not None:
info[field] = value info[field] = value
# logger.debug(f"Reagents from form: {reagents}")
for item in self.recover: for item in self.recover:
if hasattr(self, item): if hasattr(self, item):
value = getattr(self, item) value = getattr(self, item)
@@ -865,7 +803,6 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
@report_result @report_result
def create_new_submission(self, *args) -> Report: def create_new_submission(self, *args) -> Report:
pyd = self.to_pydantic() pyd = self.to_pydantic()
logger.debug(f"Pydantic: {pyd}")
sql = pyd.to_sql() sql = pyd.to_sql()
for sample in pyd.sample: for sample in pyd.sample:
if isinstance(sample, PydSample): if isinstance(sample, PydSample):
@@ -874,9 +811,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
if sample.sample_id.lower() in ["", "blank"]: if sample.sample_id.lower() in ["", "blank"]:
continue continue
sample.save() sample.save()
# if sample not in sql.sample:
sql.add_sample(sample=sample) sql.add_sample(sample=sample)
logger.debug(pformat(sql.__dict__))
try: try:
del sql._misc_info['sample'] del sql._misc_info['sample']
except KeyError: except KeyError: