Updated controls to both Irida and PCR.

This commit is contained in:
lwark
2024-10-16 15:07:43 -05:00
parent 066d1af0f2
commit c3a4aac68b
11 changed files with 750 additions and 314 deletions

View File

@@ -4,7 +4,7 @@ Constructs main application.
from pprint import pformat
from PyQt6.QtWidgets import (
QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QScrollArea, QMainWindow,
QHBoxLayout, QScrollArea, QMainWindow,
QToolBar
)
from PyQt6.QtGui import QAction
@@ -13,7 +13,7 @@ from pathlib import Path
from markdown import markdown
from __init__ import project_path
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
from .functions import select_save_file,select_open_file
from .functions import select_save_file, select_open_file
from datetime import date
from .pop_ups import HTMLPop, AlertPop
from .misc import LogParser, Pagifier
@@ -29,6 +29,7 @@ from .summary import Summary
logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger")
class App(QMainWindow):
def __init__(self, ctx: Settings = None):
@@ -61,11 +62,11 @@ class App(QMainWindow):
self.show()
self.statusBar().showMessage('Ready', 5000)
self.backup_database()
def _createMenuBar(self):
"""
adds items to menu bar
"""
"""
# logger.debug(f"Creating menu bar...")
menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File")
@@ -85,11 +86,11 @@ class App(QMainWindow):
# reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction)
def _createToolBar(self):
"""
adds items to toolbar
"""
"""
# logger.debug(f"Creating toolbar...")
toolbar = QToolBar("My main toolbar")
self.addToolBar(toolbar)
@@ -100,7 +101,7 @@ class App(QMainWindow):
def _createActions(self):
"""
creates actions
"""
"""
# logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self)
self.addReagentAction = QAction("Add Reagent", self)
@@ -139,7 +140,7 @@ class App(QMainWindow):
def showAbout(self):
"""
Show the 'about' message
"""
"""
j_env = jinja_template_loading()
template = j_env.get_template("project.html")
html = template.render(info=self.ctx.package.__dict__)
@@ -150,7 +151,7 @@ class App(QMainWindow):
def openDocs(self):
"""
Open the documentation html pages
"""
"""
if check_if_app():
url = Path(sys._MEIPASS).joinpath("files", "docs", "index.html")
else:
@@ -182,14 +183,14 @@ class App(QMainWindow):
def runSampleSearch(self):
"""
Create a search for samples.
"""
"""
dlg = SearchBox(self)
dlg.exec()
def backup_database(self):
"""
Copies the database into the backup directory the first time it is opened every month.
"""
"""
month = date.today().strftime("%Y-%m")
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve()
logger.info(f"Here is the db directory: {self.ctx.database_path}")
@@ -244,8 +245,8 @@ class App(QMainWindow):
class AddSubForm(QWidget):
def __init__(self, parent:QWidget):
def __init__(self, parent: QWidget):
# logger.debug(f"Initializating subform...")
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
@@ -255,11 +256,12 @@ class AddSubForm(QWidget):
self.tab2 = QWidget()
self.tab3 = QWidget()
self.tab4 = QWidget()
self.tabs.resize(300,200)
self.tabs.resize(300, 200)
# NOTE: Add tabs
self.tabs.addTab(self.tab1,"Submissions")
self.tabs.addTab(self.tab2,"Controls")
self.tabs.addTab(self.tab3, "Summary Report")
self.tabs.addTab(self.tab1, "Submissions")
self.tabs.addTab(self.tab2, "Irida Controls")
self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report")
# self.tabs.addTab(self.tab4, "Add Kit")
# NOTE: Create submission adder form
self.formwidget = SubmissionFormContainer(self)
@@ -276,7 +278,7 @@ class AddSubForm(QWidget):
self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout)
self.sub_wid = SubmissionsSheet(parent=parent)
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.pager)
# NOTE: Create layout of first tab to hold form and sheet
@@ -285,15 +287,19 @@ class AddSubForm(QWidget):
self.tab1.layout.addWidget(self.interior)
self.tab1.layout.addWidget(self.sheetwidget)
self.tab2.layout = QVBoxLayout(self)
self.controls_viewer = ControlsViewer(self)
self.tab2.layout.addWidget(self.controls_viewer)
self.irida_viewer = ControlsViewer(self, archetype="Irida Control")
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.tab3.layout.addWidget(self.pcr_viewer)
self.tab3.setLayout(self.tab3.layout)
# NOTE: create custom widget to add new tabs
# ST_adder = SubmissionTypeAdder(self)
summary_report = Summary(self)
self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(summary_report)
self.tab3.setLayout(self.tab3.layout)
self.tab4.layout = QVBoxLayout(self)
self.tab4.layout.addWidget(summary_report)
self.tab4.setLayout(self.tab4.layout)
# kit_adder = KitAdder(self)
# self.tab4.layout = QVBoxLayout(self)
# self.tab4.layout.addWidget(kit_adder)

View File

@@ -4,6 +4,7 @@ Handles display of control charts
import re
import sys
from datetime import timedelta, date
from pprint import pformat
from typing import Tuple
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
@@ -11,20 +12,26 @@ from PyQt6.QtWidgets import (
QDateEdit, QLabel, QSizePolicy, QPushButton, QGridLayout
)
from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control
from backend.db import ControlType, IridaControl
from PyQt6.QtCore import QDate, QSize
import logging
from pandas import DataFrame
from tools import Report, Result, get_unique_values_in_df_column, Settings, report_result
from frontend.visualizations.control_charts import CustomFigure
from frontend.visualizations import IridaFigure, PCRFigure
from .misc import StartEndDatePicker
logger = logging.getLogger(f"submissions.{__name__}")
class ControlsViewer(QWidget):
def __init__(self, parent: QWidget) -> None:
def __init__(self, parent: QWidget, archetype: str) -> None:
super().__init__(parent)
logger.debug(f"Incoming Archetype: {archetype}")
self.archetype = ControlType.query(name=archetype)
if not self.archetype:
return
logger.debug(f"Archetype set as: {self.archetype}")
self.app = self.parent().parent()
# logger.debug(f"\n\n{self.app}\n\n")
self.report = Report()
@@ -32,51 +39,53 @@ class ControlsViewer(QWidget):
self.webengineview = QWebEngineView()
# NOTE: set tab2 layout
self.layout = QGridLayout(self)
self.control_typer = QComboBox()
self.control_sub_typer = QComboBox()
# NOTE: fetch types of controls
con_types = [item.name for item in ControlType.query()]
self.control_typer.addItems(con_types)
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
self.mode_typer = QComboBox()
mode_types = Control.get_modes()
mode_types = IridaControl.get_modes()
self.mode_typer.addItems(mode_types)
# NOTE: create custom widget to get subtypes of analysis
self.sub_typer = QComboBox()
self.sub_typer.setEnabled(False)
self.mode_sub_typer = QComboBox()
self.mode_sub_typer.setEnabled(False)
# NOTE: add widgets to tab2 layout
self.layout.addWidget(self.datepicker, 0,0,1,2)
self.layout.addWidget(self.datepicker, 0, 0, 1, 2)
self.save_button = QPushButton("Save Chart", parent=self)
self.layout.addWidget(self.save_button, 0,2,1,1)
self.layout.addWidget(self.control_typer, 1,0,1,3)
self.layout.addWidget(self.mode_typer, 2,0,1,3)
self.layout.addWidget(self.sub_typer, 3,0,1,3)
self.layout.addWidget(self.webengineview, 4,0,1,3)
self.layout.addWidget(self.save_button, 0, 2, 1, 1)
self.layout.addWidget(self.control_sub_typer, 1, 0, 1, 3)
self.layout.addWidget(self.mode_typer, 2, 0, 1, 3)
self.layout.addWidget(self.mode_sub_typer, 3, 0, 1, 3)
self.archetype.get_instance_class().make_parent_buttons(parent=self)
self.layout.addWidget(self.webengineview, self.layout.rowCount(), 0, 1, 3)
self.setLayout(self.layout)
self.controls_getter()
self.control_typer.currentIndexChanged.connect(self.controls_getter)
self.mode_typer.currentIndexChanged.connect(self.controls_getter)
self.datepicker.start_date.dateChanged.connect(self.controls_getter)
self.datepicker.end_date.dateChanged.connect(self.controls_getter)
self.controls_getter_function()
self.control_sub_typer.currentIndexChanged.connect(self.controls_getter_function)
self.mode_typer.currentIndexChanged.connect(self.controls_getter_function)
self.datepicker.start_date.dateChanged.connect(self.controls_getter_function)
self.datepicker.end_date.dateChanged.connect(self.controls_getter_function)
self.save_button.pressed.connect(self.save_chart_function)
def save_chart_function(self):
self.fig.save_figure(parent=self)
def controls_getter(self):
"""
Lookup controls from database and send to chartmaker
"""
self.controls_getter_function()
# def controls_getter(self):
# """
# Lookup controls from database and send to chartmaker
# """
# self.controls_getter_function()
@report_result
def controls_getter_function(self):
def controls_getter_function(self, *args, **kwargs):
"""
Get controls based on start/end dates
"""
report = Report()
# NOTE: subtype defaults to disabled
# NOTE: mode_sub_type defaults to disabled
try:
self.sub_typer.disconnect()
self.mode_sub_typer.disconnect()
except TypeError:
pass
# NOTE: correct start date being more recent than end date and rerun
@@ -93,37 +102,32 @@ class ControlsViewer(QWidget):
# NOTE: convert to python useable date objects
self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate()
self.con_type = self.control_typer.currentText()
self.con_sub_type = self.control_sub_typer.currentText()
self.mode = self.mode_typer.currentText()
self.sub_typer.clear()
self.mode_sub_typer.clear()
# NOTE: lookup subtypes
try:
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
sub_types = self.archetype.get_modes(mode=self.mode)
except AttributeError:
sub_types = []
if sub_types != []:
# NOTE: block signal that will rerun controls getter and update sub_typer
with QSignalBlocker(self.sub_typer) as blocker:
self.sub_typer.addItems(sub_types)
self.sub_typer.setEnabled(True)
self.sub_typer.currentTextChanged.connect(self.chart_maker)
if sub_types:
# NOTE: block signal that will rerun controls 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)
self.mode_sub_typer.currentTextChanged.connect(self.chart_maker_function)
else:
self.sub_typer.clear()
self.sub_typer.setEnabled(False)
self.chart_maker()
self.mode_sub_typer.clear()
self.mode_sub_typer.setEnabled(False)
self.chart_maker_function()
return report
def diff_month(self, d1:date, d2:date):
def diff_month(self, d1: date, d2: date):
return abs((d1.year - d2.year) * 12 + d1.month - d2.month)
def chart_maker(self):
"""
Creates plotly charts for webview
"""
self.chart_maker_function()
@report_result
def chart_maker_function(self):
def chart_maker_function(self, *args, **kwargs):
# TODO: Generalize this by moving as much code as possible to IridaControl
"""
Create html chart for controls reporting
@@ -134,44 +138,26 @@ class ControlsViewer(QWidget):
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
# logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart
# Date: {self.start_date}\n\tEnd Date: {self.end_date}")
# NOTE: set the subtype for kraken
if self.sub_typer.currentText() == "":
self.subtype = None
# logger.debug(f"Control getter context: \n\tControl type: {self.con_sub_type}\n\tMode: {self.mode}\n\tStart \
# Date: {self.start_date}\n\tEnd Date: {self.end_date}")
# NOTE: set the mode_sub_type for kraken
if self.mode_sub_typer.currentText() == "":
self.mode_sub_type = None
else:
self.subtype = self.sub_typer.currentText()
# logger.debug(f"Subtype: {self.subtype}")
self.mode_sub_type = self.mode_sub_typer.currentText()
logger.debug(f"Subtype: {self.mode_sub_type}")
months = self.diff_month(self.start_date, self.end_date)
# NOTE: query all controls using the type/start and end dates from the gui
controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
# NOTE: if no data found from query set fig to none for reporting in webview
if controls is None:
fig = None
self.save_button.setEnabled(False)
else:
# NOTE: change each control to list of dictionaries
data = [control.convert_by_mode(mode=self.mode) for control in controls]
# NOTE: flatten data to one dimensional list
data = [item for sublist in data for item in sublist]
# logger.debug(f"Control objects going into df conversion: {type(data)}")
if not data:
report.add_result(Result(status="Critical", msg="No data found for controls in given date range."))
return
# NOTE send to dataframe creator
df = self.convert_data_list_to_df(input_df=data)
if self.subtype is None:
title = self.mode
else:
title = f"{self.mode} - {self.subtype}"
# NOTE: send dataframe to chart maker
df, modes = self.prep_df(ctx=self.app.ctx, df=df)
months = self.diff_month(self.start_date, self.end_date)
fig = CustomFigure(df=df, ytitle=title, modes=modes, parent=self, months=months)
self.save_button.setEnabled(True)
chart_settings = dict(sub_type=self.con_sub_type, start_date=self.start_date, end_date=self.end_date,
mode=self.mode,
sub_mode=self.mode_sub_type, parent=self, months=months)
_, self.fig = self.archetype.get_instance_class().make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
# if isinstance(self.fig, IridaFigure):
# self.save_button.setEnabled(True)
# logger.debug(f"Updating figure...")
self.fig = fig
# self.fig = fig
# NOTE: construct html for webview
html = fig.to_html()
html = self.fig.to_html()
# logger.debug(f"The length of html code is: {len(html)}")
self.webengineview.setHtml(html)
self.webengineview.update()
@@ -185,7 +171,7 @@ class ControlsViewer(QWidget):
Args:
ctx (dict): settings passed from gui
input_df (list[dict]): list of dictionaries containing records
subtype (str | None, optional): sub_type of submission type. Defaults to None.
mode_sub_type (str | None, optional): sub_type of submission type. Defaults to None.
Returns:
DataFrame: dataframe of controls
@@ -195,7 +181,7 @@ class ControlsViewer(QWidget):
safe = ['name', 'submitted_date', 'genus', 'target']
for column in df.columns:
if column not in safe:
if self.subtype is not None and column != self.subtype:
if self.mode_sub_type is not None and column != self.mode_sub_type:
continue
else:
safe.append(column)
@@ -338,34 +324,4 @@ class ControlsViewer(QWidget):
rerun_regex = re.compile(fr"{ctx.rerun_regex}")
exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
df = df[df.name not in exclude]
# for sample in sample_names:
# if rerun_regex.search(sample):
# first_run = re.sub(rerun_regex, "", sample)
# df = df.drop(df[df.name == first_run].index)
return df
# class ControlsDatePicker(QWidget):
# """
# custom widget to pick start and end dates for controls graphs
# """
#
# def __init__(self) -> None:
# super().__init__()
# self.start_date = QDateEdit(calendarPopup=True)
# # NOTE: start date is two months prior to end date by default
# sixmonthsago = QDate.currentDate().addDays(-180)
# self.start_date.setDate(sixmonthsago)
# self.end_date = QDateEdit(calendarPopup=True)
# self.end_date.setDate(QDate.currentDate())
# self.layout = QHBoxLayout()
# self.layout.addWidget(QLabel("Start Date"))
# self.layout.addWidget(self.start_date)
# self.layout.addWidget(QLabel("End Date"))
# self.layout.addWidget(self.end_date)
# self.setLayout(self.layout)
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
#
#
# def sizeHint(self) -> QSize:
# return QSize(80, 20)

View File

@@ -7,7 +7,7 @@ from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit, QGridLayout)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF
from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF, QSize
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from tools import is_power_user, jinja_template_loading
@@ -38,7 +38,7 @@ class SubmissionDetails(QDialog):
self.app = None
self.webview = QWebEngineView(parent=self)
self.webview.setMinimumSize(900, 500)
self.webview.setMaximumSize(900, 700)
self.webview.setMaximumWidth(900)
self.webview.loadFinished.connect(self.activate_export)
self.layout = QGridLayout()
# self.setFixedSize(900, 500)
@@ -98,9 +98,6 @@ class SubmissionDetails(QDialog):
sample = BasicSample.query(submitter_id=sample)
base_dict = sample.to_sub_dict(full_data=True)
exclude = ['submissions', 'excluded', 'colour', 'tooltip']
# try:
# base_dict['excluded'] += exclude
# except KeyError:
base_dict['excluded'] = exclude
template = sample.get_details_template()
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
@@ -131,7 +128,6 @@ class SubmissionDetails(QDialog):
html = template.render(reagent=base_dict, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"Reagent Details - {reagent.name} - {reagent.lot}")
# self.btn.setEnabled(False)
@pyqtSlot(str)
def submission_details(self, submission: str | BasicSubmission):
@@ -160,8 +156,6 @@ class SubmissionDetails(QDialog):
# logger.debug(f"Submission_details: {pformat(self.base_dict)}")
# logger.debug(f"User is power user: {is_power_user()}")
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css)
# with open("test.html", "w") as f:
# f.write(self.html)
self.webview.setHtml(self.html)
@pyqtSlot(str)
@@ -178,11 +172,6 @@ class SubmissionDetails(QDialog):
Renders submission 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")
# page_layout = QPageLayout()
# page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
# page_layout.setOrientation(QPageLayout.Orientation.Portrait)
# page_layout.setMargins(QMarginsF(25, 25, 25, 25))
# self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout)
save_pdf(obj=self, filename=fname)
class SubmissionComment(QDialog):