Added report tab with HTML and excel export.

This commit is contained in:
lwark
2024-10-04 11:30:22 -05:00
parent c5470b9062
commit c89ec2b62c
12 changed files with 295 additions and 181 deletions

View File

@@ -2,6 +2,7 @@
- Reverted details exports from docx back to pdf. - Reverted details exports from docx back to pdf.
- Large scale speedups for control chart construction. - Large scale speedups for control chart construction.
- Reports are now given their own tab and can be updated in real time.
## 202409.05 ## 202409.05

View File

@@ -593,7 +593,7 @@ class BasicSubmission(BaseClass):
return return
case item if item in self.jsons(): case item if item in self.jsons():
match key: match key:
case "custom": case "custom" | "source_plates":
existing = value existing = value
case _: case _:
# logger.debug(f"Setting JSON attribute.") # logger.debug(f"Setting JSON attribute.")

View File

@@ -1,13 +1,11 @@
''' '''
Contains functions for generating summary reports Contains functions for generating summary reports
''' '''
from PyQt6.QtCore import QMarginsF
from PyQt6.QtGui import QPageLayout, QPageSize
from pandas import DataFrame, ExcelWriter from pandas import DataFrame, ExcelWriter
import logging, re import logging
from pathlib import Path from pathlib import Path
from datetime import date, timedelta from datetime import date
from typing import List, Tuple, Any from typing import Tuple
from backend.db.models import BasicSubmission from backend.db.models import BasicSubmission
from tools import jinja_template_loading, get_first_blank_df_row, \ from tools import jinja_template_loading, get_first_blank_df_row, \
row_map row_map
@@ -21,10 +19,12 @@ env = jinja_template_loading()
class ReportMaker(object): class ReportMaker(object):
def __init__(self, start_date: date, end_date: date): def __init__(self, start_date: date, end_date: date, organizations:list|None=None):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date) self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date)
if organizations is not None:
self.subs = [sub for sub in self.subs if sub.submitting_lab.name in organizations]
self.detailed_df, self.summary_df = self.make_report_xlsx() self.detailed_df, self.summary_df = self.make_report_xlsx()
self.html = self.make_report_html(df=self.summary_df) self.html = self.make_report_html(df=self.summary_df)
@@ -35,6 +35,8 @@ class ReportMaker(object):
Returns: Returns:
DataFrame: output dataframe DataFrame: output dataframe
""" """
if not self.subs:
return DataFrame(), DataFrame()
df = DataFrame.from_records([item.to_dict(report=True) for item in self.subs]) df = DataFrame.from_records([item.to_dict(report=True) for item in self.subs])
# NOTE: put submissions with the same lab together # NOTE: put submissions with the same lab together
df = df.sort_values("submitting_lab") df = df.sort_values("submitting_lab")
@@ -100,17 +102,6 @@ class ReportMaker(object):
if isinstance(filename, str): if isinstance(filename, str):
filename = Path(filename) filename = Path(filename)
filename = filename.absolute() filename = filename.absolute()
# NOTE: html_to_pdf doesn't function without a PyQt6 app
# if isinstance(obj, QWidget):
# logger.info(f"We're in PyQt environment, writing PDF to: {filename}")
# 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)
# else:
# logger.info("Not in PyQt. Skipping PDF writing.")
# logger.debug("Finished writing.")
self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl') self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl')
self.summary_df.to_excel(self.writer, sheet_name="Report") self.summary_df.to_excel(self.writer, sheet_name="Report")
self.detailed_df.to_excel(self.writer, sheet_name="Details", index=False) self.detailed_df.to_excel(self.writer, sheet_name="Details", index=False)

View File

@@ -70,7 +70,6 @@ class RSLNamer(object):
logger.debug(f"Using string method for {filename}.") logger.debug(f"Using string method for {filename}.")
logger.debug(f"Using regex: {regex}") logger.debug(f"Using regex: {regex}")
m = regex.search(filename) m = regex.search(filename)
print(m)
try: try:
submission_type = m.lastgroup submission_type = m.lastgroup
logger.debug(f"Got submission type: {submission_type}") logger.debug(f"Got submission type: {submission_type}")
@@ -98,7 +97,6 @@ class RSLNamer(object):
message="Please select submission type from list below.", obj_type=SubmissionType) message="Please select submission type from list below.", obj_type=SubmissionType)
if dlg.exec(): if dlg.exec():
submission_type = dlg.parse_form() submission_type = dlg.parse_form()
print(submission_type)
submission_type = submission_type.replace("_", " ") submission_type = submission_type.replace("_", " ")
return submission_type return submission_type

View File

@@ -24,6 +24,7 @@ from .controls_chart import ControlsViewer
from .kit_creator import KitAdder from .kit_creator import KitAdder
from .submission_type_creator import SubmissionTypeAdder, SubmissionType from .submission_type_creator import SubmissionTypeAdder, SubmissionType
from .sample_search import SearchBox from .sample_search import SearchBox
from .summary import Summary
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger") logger.info("Hello, I am a logger")
@@ -69,7 +70,7 @@ class App(QMainWindow):
fileMenu = menuBar.addMenu("&File") fileMenu = menuBar.addMenu("&File")
# NOTE: Creating menus using a title # NOTE: Creating menus using a title
methodsMenu = menuBar.addMenu("&Methods") methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports") # reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly") maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help") helpMenu = menuBar.addMenu("&Help")
helpMenu.addAction(self.helpAction) helpMenu.addAction(self.helpAction)
@@ -80,7 +81,7 @@ class App(QMainWindow):
fileMenu.addAction(self.yamlImportAction) fileMenu.addAction(self.yamlImportAction)
methodsMenu.addAction(self.searchLog) methodsMenu.addAction(self.searchLog)
methodsMenu.addAction(self.searchSample) methodsMenu.addAction(self.searchSample)
reportMenu.addAction(self.generateReportAction) # reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction) maintenanceMenu.addAction(self.joinPCRAction)
@@ -102,7 +103,7 @@ class App(QMainWindow):
# logger.debug(f"Creating actions...") # logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self) self.importAction = QAction("&Import Submission", self)
self.addReagentAction = QAction("Add Reagent", self) self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self) # self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self) self.addKitAction = QAction("Import Kit", self)
self.addOrgAction = QAction("Import Org", self) self.addOrgAction = QAction("Import Org", self)
self.joinExtractionAction = QAction("Link Extraction Logs") self.joinExtractionAction = QAction("Link Extraction Logs")
@@ -122,7 +123,7 @@ class App(QMainWindow):
# logger.debug(f"Connecting actions...") # logger.debug(f"Connecting actions...")
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission) self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent) self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report) # self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions) self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr) self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
self.helpAction.triggered.connect(self.showAbout) self.helpAction.triggered.connect(self.showAbout)
@@ -254,8 +255,8 @@ class AddSubForm(QWidget):
# NOTE: Add tabs # NOTE: Add tabs
self.tabs.addTab(self.tab1,"Submissions") self.tabs.addTab(self.tab1,"Submissions")
self.tabs.addTab(self.tab2,"Controls") self.tabs.addTab(self.tab2,"Controls")
self.tabs.addTab(self.tab3, "Add SubmissionType") self.tabs.addTab(self.tab3, "Summary Report")
self.tabs.addTab(self.tab4, "Add Kit") # self.tabs.addTab(self.tab4, "Add Kit")
# NOTE: Create submission adder form # NOTE: Create submission adder form
self.formwidget = SubmissionFormContainer(self) self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self) self.formlayout = QVBoxLayout(self)
@@ -282,14 +283,15 @@ class AddSubForm(QWidget):
self.tab2.layout.addWidget(self.controls_viewer) self.tab2.layout.addWidget(self.controls_viewer)
self.tab2.setLayout(self.tab2.layout) self.tab2.setLayout(self.tab2.layout)
# NOTE: create custom widget to add new tabs # NOTE: create custom widget to add new tabs
ST_adder = SubmissionTypeAdder(self) # ST_adder = SubmissionTypeAdder(self)
summary_report = Summary(self)
self.tab3.layout = QVBoxLayout(self) self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(ST_adder) self.tab3.layout.addWidget(summary_report)
self.tab3.setLayout(self.tab3.layout) self.tab3.setLayout(self.tab3.layout)
kit_adder = KitAdder(self) # kit_adder = KitAdder(self)
self.tab4.layout = QVBoxLayout(self) # self.tab4.layout = QVBoxLayout(self)
self.tab4.layout.addWidget(kit_adder) # self.tab4.layout.addWidget(kit_adder)
self.tab4.setLayout(self.tab4.layout) # self.tab4.setLayout(self.tab4.layout)
# NOTE: add tabs to main widget # NOTE: add tabs to main widget
self.layout.addWidget(self.tabs) self.layout.addWidget(self.tabs)
self.setLayout(self.layout) self.setLayout(self.layout)

View File

@@ -8,7 +8,7 @@ from typing import Tuple
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
QDateEdit, QLabel, QSizePolicy, QPushButton QDateEdit, QLabel, QSizePolicy, QPushButton, QGridLayout
) )
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control from backend.db import ControlType, Control
@@ -17,7 +17,7 @@ import logging
from pandas import DataFrame from pandas import DataFrame
from tools import Report, Result, get_unique_values_in_df_column, Settings, report_result from tools import Report, Result, get_unique_values_in_df_column, Settings, report_result
from frontend.visualizations.control_charts import CustomFigure from frontend.visualizations.control_charts import CustomFigure
from .misc import StartEndDatePicker
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -28,10 +28,10 @@ class ControlsViewer(QWidget):
self.app = self.parent().parent() self.app = self.parent().parent()
# logger.debug(f"\n\n{self.app}\n\n") # logger.debug(f"\n\n{self.app}\n\n")
self.report = Report() self.report = Report()
self.datepicker = ControlsDatePicker() self.datepicker = StartEndDatePicker(default_start=-180)
self.webengineview = QWebEngineView() self.webengineview = QWebEngineView()
# NOTE: set tab2 layout # NOTE: set tab2 layout
self.layout = QVBoxLayout(self) self.layout = QGridLayout(self)
self.control_typer = QComboBox() self.control_typer = QComboBox()
# NOTE: fetch types of controls # NOTE: fetch types of controls
con_types = [item.name for item in ControlType.query()] con_types = [item.name for item in ControlType.query()]
@@ -44,18 +44,20 @@ class ControlsViewer(QWidget):
self.sub_typer = QComboBox() self.sub_typer = QComboBox()
self.sub_typer.setEnabled(False) self.sub_typer.setEnabled(False)
# NOTE: add widgets to tab2 layout # NOTE: add widgets to tab2 layout
self.layout.addWidget(self.datepicker) self.layout.addWidget(self.datepicker, 0,0,1,2)
self.layout.addWidget(self.control_typer) self.save_button = QPushButton("Save Chart", parent=self)
self.layout.addWidget(self.mode_typer) self.layout.addWidget(self.save_button, 0,2,1,1)
self.layout.addWidget(self.sub_typer) self.layout.addWidget(self.control_typer, 1,0,1,3)
self.layout.addWidget(self.webengineview) 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.setLayout(self.layout) self.setLayout(self.layout)
self.controls_getter() self.controls_getter()
self.control_typer.currentIndexChanged.connect(self.controls_getter) self.control_typer.currentIndexChanged.connect(self.controls_getter)
self.mode_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.start_date.dateChanged.connect(self.controls_getter)
self.datepicker.end_date.dateChanged.connect(self.controls_getter) self.datepicker.end_date.dateChanged.connect(self.controls_getter)
self.datepicker.save_button.pressed.connect(self.save_chart_function) self.save_button.pressed.connect(self.save_chart_function)
def save_chart_function(self): def save_chart_function(self):
self.fig.save_figure(parent=self) self.fig.save_figure(parent=self)
@@ -141,7 +143,7 @@ class ControlsViewer(QWidget):
# NOTE: if no data found from query set fig to none for reporting in webview # NOTE: if no data found from query set fig to none for reporting in webview
if controls is None: if controls is None:
fig = None fig = None
self.datepicker.save_button.setEnabled(False) self.save_button.setEnabled(False)
else: else:
# NOTE: change each control to list of dictionaries # NOTE: change each control to list of dictionaries
data = [control.convert_by_mode(mode=self.mode) for control in controls] data = [control.convert_by_mode(mode=self.mode) for control in controls]
@@ -160,7 +162,7 @@ class ControlsViewer(QWidget):
# NOTE: send dataframe to chart maker # NOTE: send dataframe to chart maker
df, modes = self.prep_df(ctx=self.app.ctx, df=df) df, modes = self.prep_df(ctx=self.app.ctx, df=df)
fig = CustomFigure(df=df, ytitle=title, modes=modes, parent=self) fig = CustomFigure(df=df, ytitle=title, modes=modes, parent=self)
self.datepicker.save_button.setEnabled(True) self.save_button.setEnabled(True)
# logger.debug(f"Updating figure...") # logger.debug(f"Updating figure...")
self.fig = fig self.fig = fig
# NOTE: construct html for webview # NOTE: construct html for webview
@@ -200,8 +202,6 @@ class ControlsViewer(QWidget):
continue continue
# NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating. # NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating.
df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum') df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum')
logger.debug(df)
logger.debug(safe)
df = df[[c for c in df.columns if c in safe]] df = df[[c for c in df.columns if c in safe]]
# NOTE: move date of sample submitted on same date as previous ahead one. # NOTE: move date of sample submitted on same date as previous ahead one.
df = self.displace_date(df=df) df = self.displace_date(df=df)
@@ -340,28 +340,27 @@ class ControlsViewer(QWidget):
return df return df
class ControlsDatePicker(QWidget): # class ControlsDatePicker(QWidget):
""" # """
custom widget to pick start and end dates for controls graphs # custom widget to pick start and end dates for controls graphs
""" # """
#
def __init__(self) -> None: # def __init__(self) -> None:
super().__init__() # super().__init__()
self.start_date = QDateEdit(calendarPopup=True) # self.start_date = QDateEdit(calendarPopup=True)
# NOTE: start date is two months prior to end date by default # # NOTE: start date is two months prior to end date by default
sixmonthsago = QDate.currentDate().addDays(-180) # sixmonthsago = QDate.currentDate().addDays(-180)
self.start_date.setDate(sixmonthsago) # self.start_date.setDate(sixmonthsago)
self.end_date = QDateEdit(calendarPopup=True) # self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate()) # self.end_date.setDate(QDate.currentDate())
self.layout = QHBoxLayout() # self.layout = QHBoxLayout()
self.layout.addWidget(QLabel("Start Date")) # self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date) # self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date")) # self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date) # self.layout.addWidget(self.end_date)
self.setLayout(self.layout) # self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) # self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.save_button = QPushButton("Save Chart", parent=self) #
self.layout.addWidget(self.save_button) #
# def sizeHint(self) -> QSize:
def sizeHint(self) -> QSize: # return QSize(80, 20)
return QSize(80, 20)

View File

@@ -2,17 +2,20 @@
Contains miscellaneous widgets for frontend functions Contains miscellaneous widgets for frontend functions
''' '''
from datetime import date from datetime import date
from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QStandardItemModel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog, QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QPushButton, QFormLayout QDialogButtonBox, QDateEdit, QPushButton, QFormLayout, QWidget, QHBoxLayout, QSizePolicy
) )
from PyQt6.QtCore import Qt, QDate from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF, pyqtSlot, pyqtSignal, QEvent
from tools import jinja_template_loading from tools import jinja_template_loading
from backend.db.models import * from backend.db.models import *
import logging import logging
from .pop_ups import AlertPop from .pop_ups import AlertPop
from .functions import select_open_file from .functions import select_open_file, select_save_file
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -22,8 +25,10 @@ env = jinja_template_loading()
class AddReagentForm(QDialog): class AddReagentForm(QDialog):
""" """
dialog to add gather info about new reagent dialog to add gather info about new reagent
""" """
def __init__(self, reagent_lot:str|None=None, reagent_role: str | None=None, expiry: date | None=None, reagent_name: str | None=None) -> None:
def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
reagent_name: str | None = None) -> None:
super().__init__() super().__init__()
if reagent_lot is None: if reagent_lot is None:
reagent_lot = reagent_role reagent_lot = reagent_role
@@ -71,7 +76,8 @@ class AddReagentForm(QDialog):
self.layout.addWidget(self.name_input) self.layout.addWidget(self.name_input)
self.layout.addWidget(QLabel("Lot:")) self.layout.addWidget(QLabel("Lot:"))
self.layout.addWidget(self.lot_input) self.layout.addWidget(self.lot_input)
self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)")) self.layout.addWidget(
QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
self.layout.addWidget(self.exp_input) self.layout.addWidget(self.exp_input)
self.layout.addWidget(QLabel("Type:")) self.layout.addWidget(QLabel("Type:"))
self.layout.addWidget(self.type_input) self.layout.addWidget(self.type_input)
@@ -85,57 +91,57 @@ class AddReagentForm(QDialog):
Returns: Returns:
dict: Output info dict: Output info
""" """
return dict(name=self.name_input.currentText().strip(), return dict(name=self.name_input.currentText().strip(),
lot=self.lot_input.text().strip(), lot=self.lot_input.text().strip(),
expiry=self.exp_input.date().toPyDate(), expiry=self.exp_input.date().toPyDate(),
role=self.type_input.currentText().strip()) role=self.type_input.currentText().strip())
def update_names(self): def update_names(self):
""" """
Updates reagent names form field with examples from reagent type Updates reagent names form field with examples from reagent type
""" """
# logger.debug(self.type_input.currentText()) # logger.debug(self.type_input.currentText())
self.name_input.clear() self.name_input.clear()
lookup = Reagent.query(reagent_role=self.type_input.currentText()) lookup = Reagent.query(reagent_role=self.type_input.currentText())
self.name_input.addItems(list(set([item.name for item in lookup]))) self.name_input.addItems(list(set([item.name for item in lookup])))
class ReportDatePicker(QDialog): # class ReportDatePicker(QDialog):
""" # """
custom dialog to ask for report start/stop dates # custom dialog to ask for report start/stop dates
""" # """
def __init__(self) -> None: # def __init__(self) -> None:
super().__init__() # super().__init__()
self.setWindowTitle("Select Report Date Range") # self.setWindowTitle("Select Report Date Range")
# NOTE: make confirm/reject buttons # # NOTE: make confirm/reject buttons
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)
# NOTE: widgets to ask for dates # # NOTE: widgets to ask for dates
self.start_date = QDateEdit(calendarPopup=True) # self.start_date = QDateEdit(calendarPopup=True)
self.start_date.setObjectName("start_date") # self.start_date.setObjectName("start_date")
self.start_date.setDate(QDate.currentDate()) # self.start_date.setDate(QDate.currentDate())
self.end_date = QDateEdit(calendarPopup=True) # self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setObjectName("end_date") # self.end_date.setObjectName("end_date")
self.end_date.setDate(QDate.currentDate()) # self.end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout() # self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date")) # self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date) # self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date")) # self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date) # self.layout.addWidget(self.end_date)
self.layout.addWidget(self.buttonBox) # self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout) # self.setLayout(self.layout)
#
def parse_form(self) -> dict: # def parse_form(self) -> dict:
""" # """
Converts information in this object to a dict # Converts information in this object to a dict
#
Returns: # Returns:
dict: output dict. # dict: output dict.
""" # """
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate()) # return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
class LogParser(QDialog): class LogParser(QDialog):
@@ -160,13 +166,13 @@ class LogParser(QDialog):
def filelookup(self): def filelookup(self):
""" """
Select file to search Select file to search
""" """
self.fname = select_open_file(self, "tabular") self.fname = select_open_file(self, "tabular")
def runsearch(self): def runsearch(self):
""" """
Gets total/percent occurences of string in tabular file. Gets total/percent occurences of string in tabular file.
""" """
count: int = 0 count: int = 0
total: int = 0 total: int = 0
# logger.debug(f"Current search term: {self.phrase_looker.currentText()}") # logger.debug(f"Current search term: {self.phrase_looker.currentText()}")
@@ -177,7 +183,7 @@ class LogParser(QDialog):
for line in chunk: for line in chunk:
if self.phrase_looker.currentText().lower() in line.lower(): if self.phrase_looker.currentText().lower() in line.lower():
count += 1 count += 1
percent = (count/total)*100 percent = (count / total) * 100
msg = f"I found {count} instances of the search phrase out of {total} = {percent:.2f}%." msg = f"I found {count} instances of the search phrase out of {total} = {percent:.2f}%."
status = "Information" status = "Information"
except AttributeError: except AttributeError:
@@ -186,3 +192,56 @@ class LogParser(QDialog):
dlg = AlertPop(message=msg, status=status) dlg = AlertPop(message=msg, status=status)
dlg.exec() dlg.exec()
class StartEndDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self, default_start: int) -> None:
super().__init__()
self.start_date = QDateEdit(calendarPopup=True)
# NOTE: start date is two months prior to end date by default
default_start = QDate.currentDate().addDays(default_start)
self.start_date.setDate(default_start)
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)
def save_pdf(obj: QWebEngineView, filename: Path):
page_layout = QPageLayout()
page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
page_layout.setOrientation(QPageLayout.Orientation.Portrait)
page_layout.setMargins(QMarginsF(25, 25, 25, 25))
obj.page().printToPdf(filename.absolute().__str__(), page_layout)
# subclass
class CheckableComboBox(QComboBox):
# once there is a checkState set, it is rendered
# here we assume default Unchecked
def addItem(self, item, header: bool = False):
super(CheckableComboBox, self).addItem(item)
item: QStandardItem = self.model().item(self.count() - 1, 0)
if not header:
item.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled)
item.setCheckState(Qt.CheckState.Checked)
def itemChecked(self, index):
item = self.model().item(index, 0)
return item.checkState() == Qt.CheckState.Checked
def changed(self):
logger.debug("emitting updated")
self.updated.emit()

View File

@@ -10,8 +10,9 @@ from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from tools import is_power_user, html_to_pdf, jinja_template_loading from tools import is_power_user, jinja_template_loading
from .functions import select_save_file from .functions import select_save_file
from .misc import save_pdf
from pathlib import Path from pathlib import Path
import logging import logging
from getpass import getuser from getpass import getuser
@@ -177,11 +178,12 @@ class SubmissionDetails(QDialog):
Renders submission to html, then creates and saves .pdf file to user selected file. 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") fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf")
page_layout = QPageLayout() # page_layout = QPageLayout()
page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) # page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
page_layout.setOrientation(QPageLayout.Orientation.Portrait) # page_layout.setOrientation(QPageLayout.Orientation.Portrait)
page_layout.setMargins(QMarginsF(25, 25, 25, 25)) # page_layout.setMargins(QMarginsF(25, 25, 25, 25))
self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout) # self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout)
save_pdf(obj=self, filename=fname)
class SubmissionComment(QDialog): class SubmissionComment(QDialog):
""" """

View File

@@ -10,7 +10,7 @@ from backend.db.models import BasicSubmission
from backend.excel import ReportMaker from backend.excel import ReportMaker
from tools import Report, Result, report_result from tools import Report, Result, report_result
from .functions import select_save_file, select_open_file from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker # from .misc import ReportDatePicker
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -226,32 +226,32 @@ class SubmissionsSheet(QTableView):
report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
return report return report
@report_result # @report_result
def generate_report(self, *args): # def generate_report(self, *args):
""" # """
Make a report # Make a report
""" # """
report = Report() # report = Report()
result = self.generate_report_function() # result = self.generate_report_function()
report.add_result(result) # report.add_result(result)
return report # return report
#
def generate_report_function(self): # def generate_report_function(self):
""" # """
Generate a summary of activities for a time period # Generate a summary of activities for a time period
#
Args: # Args:
obj (QMainWindow): original app window # obj (QMainWindow): original app window
#
Returns: # Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict # Tuple[QMainWindow, dict]: Collection of new main app window and result dict
""" # """
report = Report() # report = Report()
# NOTE: ask for date ranges # # NOTE: ask for date ranges
dlg = ReportDatePicker() # dlg = ReportDatePicker()
if dlg.exec(): # if dlg.exec():
info = dlg.parse_form() # info = dlg.parse_form()
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx", extension="xlsx") # fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx", extension="xlsx")
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date']) # rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
rp.write_report(filename=fname, obj=self) # rp.write_report(filename=fname, obj=self)
return report # return report

View File

@@ -0,0 +1,78 @@
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QComboBox, QLabel
from backend.db import Organization
from backend.excel import ReportMaker
from tools import Report
from .misc import StartEndDatePicker, save_pdf, CheckableComboBox
from .functions import select_save_file
import logging
logger = logging.getLogger(f"submissions.{__name__}")
class Summary(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent)
self.app = self.parent().parent()
# logger.debug(f"\n\n{self.app}\n\n")
self.report = Report()
self.datepicker = StartEndDatePicker(default_start=-31)
self.webview = QWebEngineView()
self.datepicker.start_date.dateChanged.connect(self.get_report)
self.datepicker.end_date.dateChanged.connect(self.get_report)
self.layout = QGridLayout(self)
self.layout.addWidget(self.datepicker, 0, 0, 1, 2)
self.save_excel_button = QPushButton("Save Excel", parent=self)
self.save_excel_button.pressed.connect(self.save_excel)
self.save_pdf_button = QPushButton("Save PDF", parent=self)
self.save_pdf_button.pressed.connect(self.save_pdf)
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()]:
self.org_select.addItem(org)
self.org_select.model().itemChanged.connect(self.get_report)
# self.org_select.itemChecked.connect(self.get_report)
self.layout.addWidget(self.save_excel_button, 0, 2, 1, 1)
self.layout.addWidget(self.save_pdf_button, 0, 3, 1, 1)
self.layout.addWidget(self.webview, 2, 0, 1, 4)
self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1)
self.layout.addWidget(self.org_select, 1, 1, 1, 3)
self.setLayout(self.layout)
self.get_report()
def get_report(self):
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!")
lastmonth = self.datepicker.end_date.date().addDays(-31)
# NOTE: block signal that will rerun controls getter and set start date
# Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth)
self.get_report()
return
# 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.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
self.webview.setHtml(self.report_obj.html)
if self.report_obj.subs:
self.save_pdf_button.setEnabled(True)
self.save_excel_button.setEnabled(True)
else:
self.save_pdf_button.setEnabled(False)
self.save_excel_button.setEnabled(False)
def save_excel(self):
fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx")
self.report_obj.write_report(fname, obj=self)
def save_pdf(self):
fname = select_save_file(obj=self,
default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}",
extension="pdf")
save_pdf(obj=self.webview, filename=fname)

View File

@@ -1,10 +1,10 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Submissions Report for {{ input['start_date'] }} - {{ input['end_date'] }}</title> <title>Submissions Report for {{ input['start_date'] }} to {{ input['end_date'] }}</title>
</head> </head>
<body> <body>
<h2>Submissions Report {{ input['start_date'] }} - {{ input['end_date'] }}</h2> <h2>Submissions Report {{ input['start_date'] }} to {{ input['end_date'] }}</h2>
<br> <br>
{{ input['table'] }} {{ input['table'] }}
<br> <br>
@@ -12,10 +12,10 @@
<h3><u>{{ lab['lab'] }}:</u></h3> <h3><u>{{ lab['lab'] }}:</u></h3>
{% for kit in lab['kits'] %} {% for kit in lab['kits'] %}
<p><b>{{ kit['name'] }}</b></p> <p><b>{{ kit['name'] }}</b></p>
<p> Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p> <p>&nbsp;&nbsp;&nbsp;&nbsp;Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
{% endfor %} {% endfor %}
<p><b>Lab total:</b></p> <p><b><u>Lab total:</u></b></p>
<p> Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p> <p>&nbsp;&nbsp;&nbsp;&nbsp;Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</p>
<br> <br>
{% endfor %} {% endfor %}
</body> </body>

View File

@@ -7,7 +7,7 @@ import json
import pprint import pprint
from json import JSONDecodeError from json import JSONDecodeError
import numpy as np import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv import logging, re, yaml, sys, os, stat, platform, getpass, inspect
import pandas as pd import pandas as pd
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers from logging import handlers
@@ -17,22 +17,15 @@ from sqlalchemy import create_engine, text, MetaData
from pydantic import field_validator, BaseModel, Field from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List from typing import Any, Tuple, Literal, List
from PyQt6.QtGui import QPageSize
from PyQt6.QtWebEngineWidgets import QWebEngineView
from openpyxl.worksheet.worksheet import Worksheet
from PyQt6.QtPrintSupport import QPrinter
from __init__ import project_path from __init__ import project_path
from configparser import ConfigParser from configparser import ConfigParser
from tkinter import Tk # from tkinter import Tk for Python 3.x from tkinter import Tk # from tkinter import Tk for Python 3.x
from tkinter.filedialog import askdirectory from tkinter.filedialog import askdirectory
# from .error_messaging import parse_exception_to_message from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError
from sqlalchemy.exc import ArgumentError, IntegrityError as sqlalcIntegrityError
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# package_dir = Path(__file__).parents[2].resolve()
# package_dir = project_path
logger.debug(f"Package dir: {project_path}") logger.debug(f"Package dir: {project_path}")
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -154,11 +147,11 @@ def check_not_nan(cell_contents) -> bool:
bool: True if cell has value, else, false. bool: True if cell has value, else, false.
""" """
# NOTE: check for nan as a string first # NOTE: check for nan as a string first
exclude = ['unnamed:', 'blank', 'void'] exclude = ['unnamed:', 'blank', 'void', 'nat', 'nan', ""]
try: try:
if cell_contents.lower() in exclude: if cell_contents.lower() in exclude:
cell_contents = np.nan cell_contents = np.nan
cell_contents = cell_contents.lower() # cell_contents = cell_contents.lower()
except (TypeError, AttributeError): except (TypeError, AttributeError):
pass pass
try: try:
@@ -166,14 +159,6 @@ def check_not_nan(cell_contents) -> bool:
cell_contents = np.nan cell_contents = np.nan
except TypeError as e: except TypeError as e:
pass pass
if cell_contents == "nat":
cell_contents = np.nan
if cell_contents == 'nan':
cell_contents = np.nan
if cell_contents is None:
cell_contents = np.nan
if str(cell_contents).lower() == "none":
cell_contents = np.nan
try: try:
if pd.isnull(cell_contents): if pd.isnull(cell_contents):
cell_contents = np.nan cell_contents = np.nan
@@ -899,7 +884,6 @@ def yaml_regex_creator(loader, node):
return f"(?P<{name}>RSL(?:-|_)?{abbr}(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\sA-QS-Z]|$)?R?\d?)?)" return f"(?P<{name}>RSL(?:-|_)?{abbr}(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\sA-QS-Z]|$)?R?\d?)?)"
ctx = get_config(None) ctx = get_config(None)