Added report tab with HTML and excel export.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
78
src/submissions/frontend/widgets/summary.py
Normal file
78
src/submissions/frontend/widgets/summary.py
Normal 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)
|
||||
@@ -1,10 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<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>
|
||||
<body>
|
||||
<h2>Submissions Report {{ input['start_date'] }} - {{ input['end_date'] }}</h2>
|
||||
<h2>Submissions Report {{ input['start_date'] }} to {{ input['end_date'] }}</h2>
|
||||
<br>
|
||||
{{ input['table'] }}
|
||||
<br>
|
||||
@@ -12,10 +12,10 @@
|
||||
<h3><u>{{ lab['lab'] }}:</u></h3>
|
||||
{% for kit in lab['kits'] %}
|
||||
<p><b>{{ kit['name'] }}</b></p>
|
||||
<p> Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
|
||||
<p> Runs: {{ kit['run_count'] }}, Samples: {{ kit['sample_count'] }}, Cost: {{ "${:,.2f}".format(kit['cost']) }}</p>
|
||||
{% endfor %}
|
||||
<p><b>Lab total:</b></p>
|
||||
<p> Runs: {{ lab['total_runs'] }}, Samples: {{ lab['total_samples'] }}, Cost: {{ "${:,.2f}".format(lab['total_cost']) }}</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>
|
||||
<br>
|
||||
{% endfor %}
|
||||
</body>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user