From c89ec2b62c30f75c76cbf7d38c1ae79853dbbcb8 Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 4 Oct 2024 11:30:22 -0500 Subject: [PATCH] Added report tab with HTML and excel export. --- CHANGELOG.md | 1 + .../backend/db/models/submissions.py | 2 +- src/submissions/backend/excel/reports.py | 25 +-- .../backend/validators/__init__.py | 2 - src/submissions/frontend/widgets/app.py | 26 +-- .../frontend/widgets/controls_chart.py | 77 +++++---- src/submissions/frontend/widgets/misc.py | 157 ++++++++++++------ .../frontend/widgets/submission_details.py | 14 +- .../frontend/widgets/submission_table.py | 60 +++---- src/submissions/frontend/widgets/summary.py | 78 +++++++++ src/submissions/templates/summary_report.html | 10 +- src/submissions/tools/__init__.py | 24 +-- 12 files changed, 295 insertions(+), 181 deletions(-) create mode 100644 src/submissions/frontend/widgets/summary.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e7c6d..683b8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Reverted details exports from docx back to pdf. - Large scale speedups for control chart construction. +- Reports are now given their own tab and can be updated in real time. ## 202409.05 diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index cd4a8f2..c3134fd 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -593,7 +593,7 @@ class BasicSubmission(BaseClass): return case item if item in self.jsons(): match key: - case "custom": + case "custom" | "source_plates": existing = value case _: # logger.debug(f"Setting JSON attribute.") diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 57cb991..5e54379 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -1,13 +1,11 @@ ''' Contains functions for generating summary reports ''' -from PyQt6.QtCore import QMarginsF -from PyQt6.QtGui import QPageLayout, QPageSize from pandas import DataFrame, ExcelWriter -import logging, re +import logging from pathlib import Path -from datetime import date, timedelta -from typing import List, Tuple, Any +from datetime import date +from typing import Tuple from backend.db.models import BasicSubmission from tools import jinja_template_loading, get_first_blank_df_row, \ row_map @@ -21,10 +19,12 @@ env = jinja_template_loading() 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.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.html = self.make_report_html(df=self.summary_df) @@ -35,6 +35,8 @@ class ReportMaker(object): Returns: DataFrame: output dataframe """ + if not self.subs: + return DataFrame(), DataFrame() df = DataFrame.from_records([item.to_dict(report=True) for item in self.subs]) # NOTE: put submissions with the same lab together df = df.sort_values("submitting_lab") @@ -100,17 +102,6 @@ class ReportMaker(object): if isinstance(filename, str): filename = Path(filename) 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.summary_df.to_excel(self.writer, sheet_name="Report") self.detailed_df.to_excel(self.writer, sheet_name="Details", index=False) diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index 44598e4..ec67eac 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -70,7 +70,6 @@ class RSLNamer(object): logger.debug(f"Using string method for {filename}.") logger.debug(f"Using regex: {regex}") m = regex.search(filename) - print(m) try: submission_type = m.lastgroup 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) if dlg.exec(): submission_type = dlg.parse_form() - print(submission_type) submission_type = submission_type.replace("_", " ") return submission_type diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 2125542..e3c361d 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -24,6 +24,7 @@ from .controls_chart import ControlsViewer from .kit_creator import KitAdder from .submission_type_creator import SubmissionTypeAdder, SubmissionType from .sample_search import SearchBox +from .summary import Summary logger = logging.getLogger(f'submissions.{__name__}') logger.info("Hello, I am a logger") @@ -69,7 +70,7 @@ class App(QMainWindow): fileMenu = menuBar.addMenu("&File") # NOTE: Creating menus using a title methodsMenu = menuBar.addMenu("&Methods") - reportMenu = menuBar.addMenu("&Reports") + # reportMenu = menuBar.addMenu("&Reports") maintenanceMenu = menuBar.addMenu("&Monthly") helpMenu = menuBar.addMenu("&Help") helpMenu.addAction(self.helpAction) @@ -80,7 +81,7 @@ class App(QMainWindow): fileMenu.addAction(self.yamlImportAction) methodsMenu.addAction(self.searchLog) methodsMenu.addAction(self.searchSample) - reportMenu.addAction(self.generateReportAction) + # reportMenu.addAction(self.generateReportAction) maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinPCRAction) @@ -102,7 +103,7 @@ class App(QMainWindow): # logger.debug(f"Creating actions...") self.importAction = QAction("&Import Submission", 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.addOrgAction = QAction("Import Org", self) self.joinExtractionAction = QAction("Link Extraction Logs") @@ -122,7 +123,7 @@ class App(QMainWindow): # logger.debug(f"Connecting actions...") self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission) 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.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr) self.helpAction.triggered.connect(self.showAbout) @@ -254,8 +255,8 @@ class AddSubForm(QWidget): # NOTE: Add tabs self.tabs.addTab(self.tab1,"Submissions") self.tabs.addTab(self.tab2,"Controls") - self.tabs.addTab(self.tab3, "Add SubmissionType") - self.tabs.addTab(self.tab4, "Add Kit") + self.tabs.addTab(self.tab3, "Summary Report") + # self.tabs.addTab(self.tab4, "Add Kit") # NOTE: Create submission adder form self.formwidget = SubmissionFormContainer(self) self.formlayout = QVBoxLayout(self) @@ -282,14 +283,15 @@ class AddSubForm(QWidget): self.tab2.layout.addWidget(self.controls_viewer) self.tab2.setLayout(self.tab2.layout) # 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.addWidget(ST_adder) + self.tab3.layout.addWidget(summary_report) self.tab3.setLayout(self.tab3.layout) - kit_adder = KitAdder(self) - self.tab4.layout = QVBoxLayout(self) - self.tab4.layout.addWidget(kit_adder) - self.tab4.setLayout(self.tab4.layout) + # kit_adder = KitAdder(self) + # self.tab4.layout = QVBoxLayout(self) + # self.tab4.layout.addWidget(kit_adder) + # self.tab4.setLayout(self.tab4.layout) # NOTE: add tabs to main widget self.layout.addWidget(self.tabs) self.setLayout(self.layout) diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index 1a76e11..09aa66c 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -8,7 +8,7 @@ from typing import Tuple from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QComboBox, QHBoxLayout, - QDateEdit, QLabel, QSizePolicy, QPushButton + QDateEdit, QLabel, QSizePolicy, QPushButton, QGridLayout ) from PyQt6.QtCore import QSignalBlocker from backend.db import ControlType, Control @@ -17,7 +17,7 @@ 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 .misc import StartEndDatePicker logger = logging.getLogger(f"submissions.{__name__}") @@ -28,10 +28,10 @@ class ControlsViewer(QWidget): self.app = self.parent().parent() # logger.debug(f"\n\n{self.app}\n\n") self.report = Report() - self.datepicker = ControlsDatePicker() + self.datepicker = StartEndDatePicker(default_start=-180) self.webengineview = QWebEngineView() # NOTE: set tab2 layout - self.layout = QVBoxLayout(self) + self.layout = QGridLayout(self) self.control_typer = QComboBox() # NOTE: fetch types of controls con_types = [item.name for item in ControlType.query()] @@ -44,18 +44,20 @@ class ControlsViewer(QWidget): self.sub_typer = QComboBox() self.sub_typer.setEnabled(False) # NOTE: add widgets to tab2 layout - self.layout.addWidget(self.datepicker) - self.layout.addWidget(self.control_typer) - self.layout.addWidget(self.mode_typer) - self.layout.addWidget(self.sub_typer) - self.layout.addWidget(self.webengineview) + self.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.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.datepicker.save_button.pressed.connect(self.save_chart_function) + self.save_button.pressed.connect(self.save_chart_function) def save_chart_function(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 if controls is None: fig = None - self.datepicker.save_button.setEnabled(False) + 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] @@ -160,7 +162,7 @@ class ControlsViewer(QWidget): # NOTE: send dataframe to chart maker df, modes = self.prep_df(ctx=self.app.ctx, df=df) 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...") self.fig = fig # NOTE: construct html for webview @@ -200,8 +202,6 @@ class ControlsViewer(QWidget): continue # 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') - logger.debug(df) - logger.debug(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. df = self.displace_date(df=df) @@ -340,28 +340,27 @@ class ControlsViewer(QWidget): 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) - self.save_button = QPushButton("Save Chart", parent=self) - self.layout.addWidget(self.save_button) - - def sizeHint(self) -> QSize: - return QSize(80, 20) +# 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) diff --git a/src/submissions/frontend/widgets/misc.py b/src/submissions/frontend/widgets/misc.py index 08ae7ee..89c3db8 100644 --- a/src/submissions/frontend/widgets/misc.py +++ b/src/submissions/frontend/widgets/misc.py @@ -2,17 +2,20 @@ Contains miscellaneous widgets for frontend functions ''' from datetime import date + +from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QStandardItemModel +from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import ( QLabel, QVBoxLayout, - QLineEdit, QComboBox, QDialog, - QDialogButtonBox, QDateEdit, QPushButton, QFormLayout + QLineEdit, QComboBox, QDialog, + 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 backend.db.models import * import logging 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__}") @@ -22,8 +25,10 @@ env = jinja_template_loading() class AddReagentForm(QDialog): """ 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__() if reagent_lot is None: reagent_lot = reagent_role @@ -71,7 +76,8 @@ class AddReagentForm(QDialog): self.layout.addWidget(self.name_input) self.layout.addWidget(QLabel("Lot:")) self.layout.addWidget(self.lot_input) - self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)")) + self.layout.addWidget( + QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)")) self.layout.addWidget(self.exp_input) self.layout.addWidget(QLabel("Type:")) self.layout.addWidget(self.type_input) @@ -85,57 +91,57 @@ class AddReagentForm(QDialog): Returns: dict: Output info - """ - return dict(name=self.name_input.currentText().strip(), - lot=self.lot_input.text().strip(), + """ + return dict(name=self.name_input.currentText().strip(), + lot=self.lot_input.text().strip(), expiry=self.exp_input.date().toPyDate(), role=self.type_input.currentText().strip()) def update_names(self): """ Updates reagent names form field with examples from reagent type - """ + """ # logger.debug(self.type_input.currentText()) self.name_input.clear() lookup = Reagent.query(reagent_role=self.type_input.currentText()) self.name_input.addItems(list(set([item.name for item in lookup]))) -class ReportDatePicker(QDialog): - """ - custom dialog to ask for report start/stop dates - """ - def __init__(self) -> None: - super().__init__() - self.setWindowTitle("Select Report Date Range") - # NOTE: make confirm/reject buttons - QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel - self.buttonBox = QDialogButtonBox(QBtn) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) - # NOTE: widgets to ask for dates - self.start_date = QDateEdit(calendarPopup=True) - self.start_date.setObjectName("start_date") - self.start_date.setDate(QDate.currentDate()) - self.end_date = QDateEdit(calendarPopup=True) - self.end_date.setObjectName("end_date") - self.end_date.setDate(QDate.currentDate()) - self.layout = QVBoxLayout() - self.layout.addWidget(QLabel("Start Date")) - self.layout.addWidget(self.start_date) - self.layout.addWidget(QLabel("End Date")) - self.layout.addWidget(self.end_date) - self.layout.addWidget(self.buttonBox) - self.setLayout(self.layout) - - def parse_form(self) -> dict: - """ - Converts information in this object to a dict - - Returns: - dict: output dict. - """ - return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate()) +# class ReportDatePicker(QDialog): +# """ +# custom dialog to ask for report start/stop dates +# """ +# def __init__(self) -> None: +# super().__init__() +# self.setWindowTitle("Select Report Date Range") +# # NOTE: make confirm/reject buttons +# QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel +# self.buttonBox = QDialogButtonBox(QBtn) +# self.buttonBox.accepted.connect(self.accept) +# self.buttonBox.rejected.connect(self.reject) +# # NOTE: widgets to ask for dates +# self.start_date = QDateEdit(calendarPopup=True) +# self.start_date.setObjectName("start_date") +# self.start_date.setDate(QDate.currentDate()) +# self.end_date = QDateEdit(calendarPopup=True) +# self.end_date.setObjectName("end_date") +# self.end_date.setDate(QDate.currentDate()) +# self.layout = QVBoxLayout() +# self.layout.addWidget(QLabel("Start Date")) +# self.layout.addWidget(self.start_date) +# self.layout.addWidget(QLabel("End Date")) +# self.layout.addWidget(self.end_date) +# self.layout.addWidget(self.buttonBox) +# self.setLayout(self.layout) +# +# def parse_form(self) -> dict: +# """ +# Converts information in this object to a dict +# +# Returns: +# dict: output dict. +# """ +# return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate()) class LogParser(QDialog): @@ -160,13 +166,13 @@ class LogParser(QDialog): def filelookup(self): """ Select file to search - """ + """ self.fname = select_open_file(self, "tabular") def runsearch(self): """ Gets total/percent occurences of string in tabular file. - """ + """ count: int = 0 total: int = 0 # logger.debug(f"Current search term: {self.phrase_looker.currentText()}") @@ -177,7 +183,7 @@ class LogParser(QDialog): for line in chunk: if self.phrase_looker.currentText().lower() in line.lower(): 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}%." status = "Information" except AttributeError: @@ -186,3 +192,56 @@ class LogParser(QDialog): dlg = AlertPop(message=msg, status=status) 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() diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 456b5ed..554866a 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -10,8 +10,9 @@ from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF from jinja2 import TemplateNotFound 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 .misc import save_pdf from pathlib import Path import logging 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. """ 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) + # 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): """ diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 9dfb22b..a7328ba 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -10,7 +10,7 @@ from backend.db.models import BasicSubmission from backend.excel import ReportMaker from tools import Report, Result, report_result from .functions import select_save_file, select_open_file -from .misc import ReportDatePicker +# from .misc import ReportDatePicker 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')) return report - @report_result - def generate_report(self, *args): - """ - Make a report - """ - report = Report() - result = self.generate_report_function() - report.add_result(result) - return report - - def generate_report_function(self): - """ - Generate a summary of activities for a time period - - Args: - obj (QMainWindow): original app window - - Returns: - Tuple[QMainWindow, dict]: Collection of new main app window and result dict - """ - report = Report() - # NOTE: ask for date ranges - dlg = ReportDatePicker() - if dlg.exec(): - info = dlg.parse_form() - 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.write_report(filename=fname, obj=self) - return report + # @report_result + # def generate_report(self, *args): + # """ + # Make a report + # """ + # report = Report() + # result = self.generate_report_function() + # report.add_result(result) + # return report + # + # def generate_report_function(self): + # """ + # Generate a summary of activities for a time period + # + # Args: + # obj (QMainWindow): original app window + # + # Returns: + # Tuple[QMainWindow, dict]: Collection of new main app window and result dict + # """ + # report = Report() + # # NOTE: ask for date ranges + # dlg = ReportDatePicker() + # if dlg.exec(): + # info = dlg.parse_form() + # 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.write_report(filename=fname, obj=self) + # return report diff --git a/src/submissions/frontend/widgets/summary.py b/src/submissions/frontend/widgets/summary.py new file mode 100644 index 0000000..cf12c13 --- /dev/null +++ b/src/submissions/frontend/widgets/summary.py @@ -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) diff --git a/src/submissions/templates/summary_report.html b/src/submissions/templates/summary_report.html index 713c468..46020c3 100644 --- a/src/submissions/templates/summary_report.html +++ b/src/submissions/templates/summary_report.html @@ -1,10 +1,10 @@ - Submissions Report for {{ input['start_date'] }} - {{ input['end_date'] }} + Submissions Report for {{ input['start_date'] }} to {{ input['end_date'] }} -

Submissions Report {{ input['start_date'] }} - {{ input['end_date'] }}

+

Submissions Report {{ input['start_date'] }} to {{ input['end_date'] }}


{{ input['table'] }}
@@ -12,10 +12,10 @@

{{ lab['lab'] }}:

{% for kit in lab['kits'] %}

{{ kit['name'] }}

-

Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}

+

    Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}

{% endfor %} -

Lab total:

-

Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}

+

Lab total:

+

    Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}


{% endfor %} diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 40cc2f6..cb7c699 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -7,7 +7,7 @@ import json import pprint from json import JSONDecodeError 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 from jinja2 import Environment, FileSystemLoader from logging import handlers @@ -17,22 +17,15 @@ from sqlalchemy import create_engine, text, MetaData from pydantic import field_validator, BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict 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 configparser import ConfigParser from tkinter import Tk # from tkinter import Tk for Python 3.x from tkinter.filedialog import askdirectory -# from .error_messaging import parse_exception_to_message -from sqlalchemy.exc import ArgumentError, IntegrityError as sqlalcIntegrityError +from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError logger = logging.getLogger(f"submissions.{__name__}") -# package_dir = Path(__file__).parents[2].resolve() -# package_dir = project_path logger.debug(f"Package dir: {project_path}") if platform.system() == "Windows": @@ -154,11 +147,11 @@ def check_not_nan(cell_contents) -> bool: bool: True if cell has value, else, false. """ # NOTE: check for nan as a string first - exclude = ['unnamed:', 'blank', 'void'] + exclude = ['unnamed:', 'blank', 'void', 'nat', 'nan', ""] try: if cell_contents.lower() in exclude: cell_contents = np.nan - cell_contents = cell_contents.lower() + # cell_contents = cell_contents.lower() except (TypeError, AttributeError): pass try: @@ -166,14 +159,6 @@ def check_not_nan(cell_contents) -> bool: cell_contents = np.nan except TypeError as e: 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: if pd.isnull(cell_contents): 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?)?)" - ctx = get_config(None)