Added report tab with HTML and excel export.

This commit is contained in:
lwark
2024-10-04 15:24:00 -05:00
parent c89ec2b62c
commit 5fe5c22222
5 changed files with 106 additions and 88 deletions

View File

@@ -13,7 +13,7 @@ from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter from operator import itemgetter
from pprint import pformat from pprint import pformat
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, desc
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@@ -283,7 +283,7 @@ class BasicSubmission(BaseClass):
del input_dict['id'] del input_dict['id']
return input_dict return input_dict
def generate_associations(self, name:str, extra:str|None=None): def generate_associations(self, name: str, extra: str | None = None):
try: try:
field = self.__getattribute__(name) field = self.__getattribute__(name)
except AttributeError: except AttributeError:
@@ -479,15 +479,6 @@ class BasicSubmission(BaseClass):
Returns: Returns:
str: html output string. str: html output string.
""" """
# output_samples = []
# logger.debug("Setting locations.")
# for column in range(1, plate_columns + 1):
# for row in range(1, plate_rows + 1):
# try:
# well = next((item for item in sample_list if item['row'] == row and item['column'] == column), dict(name="", row=row, column=column, background_color="#ffffff"))
# except StopIteration:
# well = dict(name="", row=row, column=column, background_color="#ffffff")
# output_samples.append(well)
rows = range(1, plate_rows + 1) rows = range(1, plate_rows + 1)
columns = range(1, plate_columns + 1) columns = range(1, plate_columns + 1)
# NOTE: An overly complicated list comprehension create a list of sample locations # NOTE: An overly complicated list comprehension create a list of sample locations
@@ -512,11 +503,12 @@ class BasicSubmission(BaseClass):
@classmethod @classmethod
def submissions_to_df(cls, submission_type: str | None = None, limit: int = 0, def submissions_to_df(cls, submission_type: str | None = None, limit: int = 0,
chronologic: bool = True) -> pd.DataFrame: chronologic: bool = True, page: int = 1, page_size: int = 250) -> pd.DataFrame:
""" """
Convert all submissions to dataframe Convert all submissions to dataframe
Args: Args:
page (int, optional): Limits the number of submissions to a page size. Defaults to 1.
chronologic (bool, optional): Sort submissions in chronologic order. Defaults to True. chronologic (bool, optional): Sort submissions in chronologic order. Defaults to True.
submission_type (str | None, optional): Filter by SubmissionType. Defaults to None. submission_type (str | None, optional): Filter by SubmissionType. Defaults to None.
limit (int, optional): Maximum number of results to return. Defaults to 0. limit (int, optional): Maximum number of results to return. Defaults to 0.
@@ -528,16 +520,16 @@ class BasicSubmission(BaseClass):
# logger.debug(f"Using limit: {limit}") # logger.debug(f"Using limit: {limit}")
# NOTE: use lookup function to create list of dicts # NOTE: use lookup function to create list of dicts
subs = [item.to_dict() for item in subs = [item.to_dict() for item in
cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic)] cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic, page=page, page_size=page_size)]
# logger.debug(f"Got {len(subs)} submissions.") # logger.debug(f"Got {len(subs)} submissions.")
df = pd.DataFrame.from_records(subs) df = pd.DataFrame.from_records(subs)
# logger.debug(f"Column names: {df.columns}") # logger.debug(f"Column names: {df.columns}")
# NOTE: Exclude sub information # NOTE: Exclude sub information
exclude = ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents', exclude = ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents',
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls', 'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls',
'source_plates', 'pcr_technician', 'ext_technician', 'artic_technician', 'cost_centre', 'source_plates', 'pcr_technician', 'ext_technician', 'artic_technician', 'cost_centre',
'signed_by', 'artic_date', 'gel_barcode', 'gel_date', 'ngs_date', 'contact_phone', 'contact', 'signed_by', 'artic_date', 'gel_barcode', 'gel_date', 'ngs_date', 'contact_phone', 'contact',
'tips', 'gel_image_path', 'custom'] 'tips', 'gel_image_path', 'custom']
df = df.loc[:, ~df.columns.isin(exclude)] df = df.loc[:, ~df.columns.isin(exclude)]
# for item in excluded: # for item in excluded:
# try: # try:
@@ -829,7 +821,7 @@ class BasicSubmission(BaseClass):
return input_dict return input_dict
@classmethod @classmethod
def custom_validation(cls, pyd:"PydSubmission") -> dict: def custom_validation(cls, pyd: "PydSubmission") -> dict:
""" """
Performs any final custom parsing of the excel file. Performs any final custom parsing of the excel file.
@@ -886,7 +878,6 @@ class BasicSubmission(BaseClass):
ws.cell(row=item['row'], column=item['column'], value=item['value']) ws.cell(row=item['row'], column=item['column'], value=item['value'])
return input_excel return input_excel
@classmethod @classmethod
def enforce_name(cls, instr: str, data: dict | None = {}) -> str: def enforce_name(cls, instr: str, data: dict | None = {}) -> str:
""" """
@@ -1061,6 +1052,8 @@ class BasicSubmission(BaseClass):
reagent: Reagent | str | None = None, reagent: Reagent | str | None = None,
chronologic: bool = False, chronologic: bool = False,
limit: int = 0, limit: int = 0,
page: int = 1,
page_size: int = 250,
**kwargs **kwargs
) -> BasicSubmission | List[BasicSubmission]: ) -> BasicSubmission | List[BasicSubmission]:
""" """
@@ -1161,7 +1154,13 @@ class BasicSubmission(BaseClass):
case _: case _:
pass pass
if chronologic: if chronologic:
query.order_by(cls.submitted_date) logger.debug("Attempting sort by date descending")
query = query.order_by(cls.submitted_date.desc())
if page_size is not None:
query = query.limit(page_size)
page = page - 1
if page is not None:
query = query.offset(page * page_size)
return cls.execute_query(query=query, model=model, limit=limit, **kwargs) return cls.execute_query(query=query, model=model, limit=limit, **kwargs)
@classmethod @classmethod
@@ -2483,7 +2482,7 @@ class BasicSample(BaseClass):
df = pd.DataFrame.from_records(samples) df = pd.DataFrame.from_records(samples)
# NOTE: Exclude sub information # NOTE: Exclude sub information
exclude = ['concentration', 'organism', 'colour', 'tooltip', 'comments', 'samples', 'reagents', exclude = ['concentration', 'organism', 'colour', 'tooltip', 'comments', 'samples', 'reagents',
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls'] 'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls']
df = df.loc[:, ~df.columns.isin(exclude)] df = df.loc[:, ~df.columns.isin(exclude)]
return df return df

View File

@@ -611,7 +611,7 @@ class PydSubmission(BaseModel, extra='allow'):
@classmethod @classmethod
def expand_samples(cls, value): def expand_samples(cls, value):
if isinstance(value, Generator): if isinstance(value, Generator):
# logger.debug("We have a generator") # logger.debug("We have a generator")[
return [PydSample(**sample) for sample in value] return [PydSample(**sample) for sample in value]
return value return value

View File

@@ -12,11 +12,11 @@ from pathlib import Path
from markdown import markdown from markdown import markdown
from __init__ import project_path from __init__ import project_path
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
from .functions import select_save_file,select_open_file from .functions import select_save_file,select_open_file
from datetime import date from datetime import date
from .pop_ups import HTMLPop, AlertPop from .pop_ups import HTMLPop, AlertPop
from .misc import LogParser from .misc import LogParser, Pagifier
import logging, webbrowser, sys, shutil import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
@@ -49,6 +49,7 @@ class App(QMainWindow):
self.height = 1000 self.height = 1000
self.setWindowTitle(self.title) self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height) self.setGeometry(self.left, self.top, self.width, self.height)
self.page_size = page_size
# NOTE: insert tabs into main app # NOTE: insert tabs into main app
self.table_widget = AddSubForm(self) self.table_widget = AddSubForm(self)
self.setCentralWidget(self.table_widget) self.setCentralWidget(self.table_widget)
@@ -133,6 +134,7 @@ class App(QMainWindow):
self.githubAction.triggered.connect(self.openGithub) self.githubAction.triggered.connect(self.openGithub)
self.yamlExportAction.triggered.connect(self.export_ST_yaml) self.yamlExportAction.triggered.connect(self.export_ST_yaml)
self.yamlImportAction.triggered.connect(self.import_ST_yaml) self.yamlImportAction.triggered.connect(self.import_ST_yaml)
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
def showAbout(self): def showAbout(self):
""" """
@@ -237,6 +239,8 @@ class App(QMainWindow):
else: else:
logger.warning("Save of submission type cancelled.") logger.warning("Save of submission type cancelled.")
def update_data(self):
self.table_widget.sub_wid.setData(page=int(self.table_widget.pager.current_page.text()), page_size=page_size)
class AddSubForm(QWidget): class AddSubForm(QWidget):
@@ -272,7 +276,9 @@ class AddSubForm(QWidget):
self.sheetlayout = QVBoxLayout(self) self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout) self.sheetwidget.setLayout(self.sheetlayout)
self.sub_wid = SubmissionsSheet(parent=parent) self.sub_wid = SubmissionsSheet(parent=parent)
self.pager = Pagifier(page_max=self.sub_wid.total_count/page_size)
self.sheetlayout.addWidget(self.sub_wid) self.sheetlayout.addWidget(self.sub_wid)
self.sheetlayout.addWidget(self.pager)
# NOTE: Create layout of first tab to hold form and sheet # NOTE: Create layout of first tab to hold form and sheet
self.tab1.layout = QHBoxLayout(self) self.tab1.layout = QHBoxLayout(self)
self.tab1.setLayout(self.tab1.layout) self.tab1.setLayout(self.tab1.layout)

View File

@@ -1,21 +1,22 @@
''' '''
Contains miscellaneous widgets for frontend functions Contains miscellaneous widgets for frontend functions
''' '''
import math
from datetime import date from datetime import date
from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QStandardItemModel from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QIcon
from PyQt6.QtWebEngineWidgets import QWebEngineView 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, QWidget, QHBoxLayout, QSizePolicy QDialogButtonBox, QDateEdit, QPushButton, QFormLayout, QWidget, QHBoxLayout, QSizePolicy
) )
from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF, pyqtSlot, pyqtSignal, QEvent from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF
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, select_save_file from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -245,3 +246,42 @@ class CheckableComboBox(QComboBox):
def changed(self): def changed(self):
logger.debug("emitting updated") logger.debug("emitting updated")
self.updated.emit() self.updated.emit()
class Pagifier(QWidget):
def __init__(self, page_max:int):
super().__init__()
self.page_max = math.ceil(page_max)
next = QPushButton(parent=self, icon = QIcon.fromTheme(QIcon.ThemeIcon.GoNext))
next.pressed.connect(self.increment_page)
previous = QPushButton(parent=self, icon=QIcon.fromTheme(QIcon.ThemeIcon.GoPrevious))
previous.pressed.connect(self.decrement_page)
label = QLabel(f"/ {self.page_max}")
label.setMinimumWidth(200)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.current_page = QLineEdit(self)
self.current_page.setEnabled(False)
# onlyInt = QIntValidator()
# onlyInt.setRange(1, 4)
# self.current_page.setValidator(onlyInt)
self.current_page.setText("1")
self.current_page.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout = QHBoxLayout()
self.layout.addWidget(previous)
self.layout.addWidget(self.current_page)
self.layout.addWidget(label)
self.layout.addWidget(next)
self.setLayout(self.layout)
def increment_page(self):
new = int(self.current_page.text())+1
if new <= self.page_max:
self.current_page.setText(str(new))
def decrement_page(self):
new = int(self.current_page.text())-1
if new >= 1:
self.current_page.setText(str(new))

View File

@@ -7,9 +7,9 @@ from PyQt6.QtWidgets import QTableView, QMenu
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor from PyQt6.QtGui import QAction, QCursor
from backend.db.models import BasicSubmission from backend.db.models import BasicSubmission
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_open_file
# from .misc import ReportDatePicker # from .misc import ReportDatePicker
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,6 +20,7 @@ class pandasModel(QAbstractTableModel):
pandas model for inserting summary sheet into gui pandas model for inserting summary sheet into gui
NOTE: Copied from Stack Overflow. I have no idea how it actually works. NOTE: Copied from Stack Overflow. I have no idea how it actually works.
""" """
def __init__(self, data) -> None: def __init__(self, data) -> None:
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
self._data = data self._data = data
@@ -48,7 +49,7 @@ class pandasModel(QAbstractTableModel):
""" """
return self._data.shape[1] return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str|None: def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str | None:
if index.isValid(): if index.isValid():
if role == Qt.ItemDataRole.DisplayRole: if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()]) return str(self._data.iloc[index.row(), index.column()])
@@ -64,6 +65,7 @@ class SubmissionsSheet(QTableView):
""" """
presents submission summary to user in tab1 presents submission summary to user in tab1
""" """
def __init__(self, parent) -> None: def __init__(self, parent) -> None:
""" """
initialize initialize
@@ -74,17 +76,18 @@ class SubmissionsSheet(QTableView):
super().__init__(parent) super().__init__(parent)
self.app = self.parent() self.app = self.parent()
self.report = Report() self.report = Report()
self.setData() self.setData(page=1, page_size=self.app.page_size)
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self)) self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
self.total_count = BasicSubmission.__database_session__.query(BasicSubmission).count()
def setData(self) -> None: def setData(self, page: int = 1, page_size: int = 250) -> None:
""" """
sets data in model sets data in model
""" """
self.data = BasicSubmission.submissions_to_df() self.data = BasicSubmission.submissions_to_df(page=page)
try: try:
self.data['Id'] = self.data['Id'].apply(str) self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4) self.data['Id'] = self.data['Id'].str.zfill(4)
@@ -103,7 +106,7 @@ class SubmissionsSheet(QTableView):
""" """
# logger.debug(event().__dict__) # logger.debug(event().__dict__)
id = self.selectionModel().currentIndex() id = self.selectionModel().currentIndex()
id = id.sibling(id.row(),0).data() id = id.sibling(id.row(), 0).data()
submission = BasicSubmission.query(id=id) submission = BasicSubmission.query(id=id)
self.menu = QMenu(self) self.menu = QMenu(self)
self.con_actions = submission.custom_context_events() self.con_actions = submission.custom_context_events()
@@ -115,7 +118,7 @@ class SubmissionsSheet(QTableView):
# add other required actions # add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
def triggered_action(self, action_name:str): def triggered_action(self, action_name: str):
""" """
Calls the triggered action from the context menu Calls the triggered action from the context menu
@@ -155,16 +158,16 @@ class SubmissionsSheet(QTableView):
count = 0 count = 0
for run in runs: for run in runs:
new_run = dict( new_run = dict(
start_time=run[0].strip(), start_time=run[0].strip(),
rsl_plate_num=run[1].strip(), rsl_plate_num=run[1].strip(),
sample_count=run[2].strip(), sample_count=run[2].strip(),
status=run[3].strip(), status=run[3].strip(),
experiment_name=run[4].strip(), experiment_name=run[4].strip(),
end_time=run[5].strip() end_time=run[5].strip()
) )
# NOTE: elution columns are item 6 in the comma split list to the end # NOTE: elution columns are item 6 in the comma split list to the end
for ii in range(6, len(run)): for ii in range(6, len(run)):
new_run[f"column{str(ii-5)}_vol"] = run[ii] new_run[f"column{str(ii - 5)}_vol"] = run[ii]
# NOTE: Lookup imported submissions # NOTE: Lookup imported submissions
sub = BasicSubmission.query(rsl_plate_num=new_run['rsl_plate_num']) sub = BasicSubmission.query(rsl_plate_num=new_run['rsl_plate_num'])
# NOTE: If no such submission exists, move onto the next run # NOTE: If no such submission exists, move onto the next run
@@ -208,13 +211,13 @@ class SubmissionsSheet(QTableView):
count = 0 count = 0
for run in runs: for run in runs:
new_run = dict( new_run = dict(
start_time=run[0].strip(), start_time=run[0].strip(),
rsl_plate_num=run[1].strip(), rsl_plate_num=run[1].strip(),
biomek_status=run[2].strip(), biomek_status=run[2].strip(),
quant_status=run[3].strip(), quant_status=run[3].strip(),
experiment_name=run[4].strip(), experiment_name=run[4].strip(),
end_time=run[5].strip() end_time=run[5].strip()
) )
# NOTE: lookup imported submission # NOTE: lookup imported submission
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num']) sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
# NOTE: if imported submission doesn't exist move on to next run # NOTE: if imported submission doesn't exist move on to next run
@@ -225,33 +228,3 @@ class SubmissionsSheet(QTableView):
sub.save() sub.save()
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
# 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