Overhauling database.

This commit is contained in:
lwark
2025-05-13 13:20:01 -05:00
parent 0dbb4ae77a
commit 75c665ea05
21 changed files with 729 additions and 1727 deletions

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path
from markdown import markdown
from pandas import ExcelWriter
from backend import Reagent, BasicSample, Organization, KitType, BasicSubmission
from backend import Reagent, BasicSample, Organization, KitType, BasicRun
from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development
@@ -211,7 +211,7 @@ class App(QMainWindow):
dlg = DateTypePicker(self)
if dlg.exec():
output = dlg.parse_form()
df = BasicSubmission.archive_submissions(**output)
df = BasicRun.archive_submissions(**output)
filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx")
writer = ExcelWriter(filepath, "openpyxl")
df.to_excel(writer)
@@ -239,7 +239,7 @@ class AddSubForm(QWidget):
self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report")
self.tabs.addTab(self.tab5, "Turnaround Times")
# NOTE: Create submission adder form
# NOTE: Create run adder form
self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout)

View File

@@ -6,7 +6,7 @@ from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
)
from backend.db.models import Equipment, BasicSubmission, Process
from backend.db.models import Equipment, BasicRun, Process
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging
from typing import Generator
@@ -16,7 +16,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, submission: BasicSubmission):
def __init__(self, parent, submission: BasicRun):
super().__init__(parent)
self.submission = submission
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
@@ -137,7 +137,7 @@ class RoleComboBox(QWidget):
if process.tip_roles:
for iii, tip_role in enumerate(process.tip_roles):
widget = QComboBox()
tip_choices = [item.name for item in tip_role.instances]
tip_choices = [item.name for item in tip_role.controls]
widget.setEditable(False)
widget.addItems(tip_choices)
widget.setObjectName(f"tips_{tip_role.name}")

View File

@@ -12,7 +12,7 @@ import logging, numpy as np
from pprint import pformat
from typing import Tuple, List
from pathlib import Path
from backend.db.models import BasicSubmission
from backend.db.models import BasicRun
logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,7 +20,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
# Main window class
class GelBox(QDialog):
def __init__(self, parent, img_path: str | Path, submission: BasicSubmission):
def __init__(self, parent, img_path: str | Path, submission: BasicRun):
super().__init__(parent)
# NOTE: setting title
self.setWindowTitle(f"Gel - {img_path}")

View File

@@ -1,5 +1,5 @@
"""
Webview to show submission and sample details.
Webview to show run and sample details.
"""
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit, QGridLayout)
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType, Equipment, Process, Tips
from backend.db.models import BasicRun, BasicSample, Reagent, KitType, Equipment, Process, Tips
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
from .functions import select_save_file, save_pdf
from pathlib import Path
@@ -23,10 +23,10 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
a window showing text details of run
"""
def __init__(self, parent, sub: BasicSubmission | BasicSample | Reagent) -> None:
def __init__(self, parent, sub: BasicRun | BasicSample | Reagent) -> None:
super().__init__(parent)
self.app = get_application_from_parent(parent)
@@ -51,8 +51,8 @@ class SubmissionDetails(QDialog):
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
match sub:
case BasicSubmission():
self.submission_details(submission=sub)
case BasicRun():
self.run_details(run=sub)
self.rsl_plate_num = sub.rsl_plate_num
case BasicSample():
self.sample_details(sample=sub)
@@ -203,52 +203,52 @@ class SubmissionDetails(QDialog):
logger.error(f"Reagent with lot {old_lot} not found.")
@pyqtSlot(str)
def submission_details(self, submission: str | BasicSubmission):
def run_details(self, run: str | BasicRun):
"""
Sets details view to summary of Submission.
Args:
submission (str | BasicSubmission): Submission of interest.
run (str | BasicRun): Submission of interest.
"""
logger.debug(f"Submission details.")
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
self.rsl_plate_num = submission.rsl_plate_num
self.base_dict = submission.to_dict(full_data=True)
if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run)
self.rsl_plate_num = run.rsl_plate_num
self.base_dict = run.to_dict(full_data=True)
# NOTE: don't want id
self.base_dict['platemap'] = submission.make_plate_map(sample_list=submission.hitpicked)
self.base_dict['excluded'] = submission.get_default_info("details_ignore")
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict)
self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked)
self.base_dict['excluded'] = run.get_default_info("details_ignore")
self.base_dict, self.template = run.get_details_template(base_dict=self.base_dict)
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
# logger.debug(f"Base dictionary of submission {self.rsl_plate_num}: {pformat(self.base_dict)}")
# logger.debug(f"Base dictionary of run {self.rsl_plate_num}: {pformat(self.base_dict)}")
self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html)
@pyqtSlot(str)
def sign_off(self, submission: str | BasicSubmission) -> None:
def sign_off(self, run: str | BasicRun) -> None:
"""
Allows power user to signify a submission is complete.
Allows power user to signify a run is complete.
Args:
submission (str | BasicSubmission): Submission to be completed
run (str | BasicRun): Submission to be completed
Returns:
None
"""
logger.info(f"Signing off on {submission} - ({getuser()})")
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
submission.signed_by = getuser()
submission.completed_date = datetime.now()
submission.completed_date.replace(tzinfo=timezone)
submission.save()
self.submission_details(submission=self.rsl_plate_num)
logger.info(f"Signing off on {run} - ({getuser()})")
if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run)
run.signed_by = getuser()
run.completed_date = datetime.now()
run.completed_date.replace(tzinfo=timezone)
run.save()
self.run_details(run=self.rsl_plate_num)
def save_pdf(self):
"""
Renders submission to html, then creates and saves .pdf file to user selected file.
Renders run 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")
save_pdf(obj=self.webview, filename=fname)
@@ -256,10 +256,10 @@ class SubmissionDetails(QDialog):
class SubmissionComment(QDialog):
"""
a window for adding comment text to a submission
a window for adding comment text to a run
"""
def __init__(self, parent, submission: BasicSubmission) -> None:
def __init__(self, parent, submission: BasicRun) -> None:
super().__init__(parent)
self.app = get_application_from_parent(parent)
@@ -282,7 +282,7 @@ class SubmissionComment(QDialog):
def parse_form(self) -> List[dict]:
"""
Adds comment to submission object.
Adds comment to run object.
"""
commenter = getuser()
comment = self.txt_editor.toPlainText()

View File

@@ -1,5 +1,5 @@
"""
Contains widgets specific to the submission summary and submission details.
Contains widgets specific to the run summary and run details.
"""
import logging
import sys
@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, Q
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor
from backend.db.models import BasicSubmission, ClientSubmission
from backend.db.models import BasicRun, ClientSubmission
from tools import Report, Result, report_result
from .functions import select_open_file
@@ -63,7 +63,7 @@ class pandasModel(QAbstractTableModel):
class SubmissionsSheet(QTableView):
"""
presents submission summary to user in tab1
presents run summary to user in tab1
"""
def __init__(self, parent) -> None:
@@ -74,20 +74,20 @@ class SubmissionsSheet(QTableView):
page_size = self.app.page_size
except AttributeError:
page_size = 250
self.setData(page=1, page_size=page_size)
self.set_data(page=1, page_size=page_size)
self.resizeColumnsToContents()
self.resizeRowsToContents()
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: BasicRun.query(id=x.sibling(x.row(), 0).data()).show_details(self))
# NOTE: Have to run native query here because mine just returns results?
self.total_count = BasicSubmission.__database_session__.query(BasicSubmission).count()
self.total_count = BasicRun.__database_session__.query(BasicRun).count()
def setData(self, page: int = 1, page_size: int = 250) -> None:
def set_data(self, page: int = 1, page_size: int = 250) -> None:
"""
sets data in model
"""
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = BasicSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = BasicRun.submissions_to_df(page=page, page_size=page_size)
try:
self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4)
@@ -108,7 +108,7 @@ class SubmissionsSheet(QTableView):
id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0)
id = id.sibling(id.row(), 0).data()
submission = BasicSubmission.query(id=id)
submission = BasicRun.query(id=id)
self.menu = QMenu(self)
self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys():
@@ -167,8 +167,8 @@ class SubmissionsSheet(QTableView):
for ii in range(6, len(run)):
new_run[f"column{str(ii - 5)}_vol"] = run[ii]
# NOTE: Lookup imported submissions
sub = BasicSubmission.query(rsl_plate_num=new_run['rsl_plate_num'])
# NOTE: If no such submission exists, move onto the next run
sub = BasicRun.query(rsl_plate_num=new_run['rsl_plate_num'])
# NOTE: If no such run exists, move onto the next run
if sub is None:
continue
try:
@@ -192,7 +192,7 @@ class SubmissionsSheet(QTableView):
def link_pcr_function(self):
"""
Link PCR data from run logs to an imported submission
Link PCR data from run logs to an imported run
Args:
obj (QMainWindow): original app window
@@ -215,9 +215,9 @@ class SubmissionsSheet(QTableView):
experiment_name=run[4].strip(),
end_time=run[5].strip()
)
# NOTE: lookup imported submission
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
# NOTE: if imported submission doesn't exist move on to next run
# NOTE: lookup imported run
sub = BasicRun.query(rsl_number=new_run['rsl_plate_num'])
# NOTE: if imported run doesn't exist move on to next run
if sub is None:
continue
sub.set_attribute('pcr_info', new_run)
@@ -302,7 +302,7 @@ class SubmissionsTree(QTreeView):
id = int(id.data())
except ValueError:
return
BasicSubmission.query(id=id).show_details(self)
BasicRun.query(id=id).show_details(self)
def link_extractions(self):

View File

@@ -1,5 +1,5 @@
"""
Contains all submission related frontend functions
Contains all run related frontend functions
"""
from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout,
@@ -10,11 +10,11 @@ from .functions import select_open_file, select_save_file
import logging
from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.excel.parsers import SheetParser, InfoParserV2
from backend.excel import SheetParser, InfoParser
from backend.validators import PydSubmission, PydReagent
from backend.db import (
Organization, SubmissionType, Reagent,
ReagentRole, KitTypeReagentRoleAssociation, BasicSubmission
ReagentRole, KitTypeReagentRoleAssociation, BasicRun
)
from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop
@@ -93,7 +93,7 @@ class SubmissionFormContainer(QWidget):
@report_result
def import_submission_function(self, fname: Path | None = None) -> Report:
"""
Import a new submission to the app window
Import a new run to the app window
Args:
obj (QMainWindow): original app window
@@ -122,12 +122,12 @@ class SubmissionFormContainer(QWidget):
# NOTE: create sheetparser using excel sheet and context from gui
try:
# self.prsr = SheetParser(filepath=fname)
self.parser = InfoParserV2(filepath=fname)
self.parser = InfoParser(filepath=fname)
except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}")
return
except AttributeError:
self.parser = InfoParserV2(filepath=fname)
self.parser = InfoParser(filepath=fname)
self.pyd = self.parser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pyd.samples)}")
checker = SampleChecker(self, "Sample Checker", self.pyd)
@@ -150,7 +150,7 @@ class SubmissionFormContainer(QWidget):
instance (Reagent | None): Blank reagent instance to be edited and then added.
Returns:
models.Reagent: the constructed reagent object to add to submission
models.Reagent: the constructed reagent object to add to run
"""
report = Report()
if not instance:
@@ -178,7 +178,7 @@ class SubmissionFormWidget(QWidget):
self.missing_info = []
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
basic_submission_class = self.submission_type.submission_class
logger.debug(f"Basic submission class: {basic_submission_class}")
logger.debug(f"Basic run class: {basic_submission_class}")
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore']
@@ -202,7 +202,7 @@ class SubmissionFormWidget(QWidget):
value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}")
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
sub_obj=basic_submission_class, disable=check)
run_object=basic_submission_class, disable=check)
if add_widget is not None:
self.layout.addWidget(add_widget)
if k in self.__class__.update_reagent_fields:
@@ -223,7 +223,7 @@ class SubmissionFormWidget(QWidget):
reagent.flip_check(self.disabler.checkbox.isChecked())
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None,
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None,
extraction_kit: str | None = None, run_object: BasicRun | None = None,
disable: bool = False) -> "self.InfoItem":
"""
Make an InfoItem widget to hold a field
@@ -248,7 +248,7 @@ class SubmissionFormWidget(QWidget):
widget = None
case _:
widget = self.InfoItem(parent=self, key=key, value=value, submission_type=submission_type,
sub_obj=sub_obj)
run_object=run_object)
if disable:
widget.input.setEnabled(False)
widget.input.setToolTip("Widget disabled to protect database integrity.")
@@ -373,14 +373,14 @@ class SubmissionFormWidget(QWidget):
return report
case _:
pass
# NOTE: add reagents to submission object
# NOTE: add reagents to run object
if base_submission is None:
return
for reagent in base_submission.reagents:
reagent.update_last_used(kit=base_submission.extraction_kit)
save_output = base_submission.save()
# NOTE: update summary sheet
self.app.table_widget.sub_wid.setData()
self.app.table_widget.sub_wid.set_data()
# NOTE: reset form
try:
check = save_output.results == []
@@ -393,7 +393,7 @@ class SubmissionFormWidget(QWidget):
def export_csv_function(self, fname: Path | None = None):
"""
Save the submission's csv file.
Save the run's csv file.
Args:
fname (Path | None, optional): Input filename. Defaults to None.
@@ -405,7 +405,7 @@ class SubmissionFormWidget(QWidget):
except PermissionError:
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
except AttributeError:
logger.error(f"No csv file found in the submission at this point.")
logger.error(f"No csv file found in the run at this point.")
def parse_form(self) -> Report:
"""
@@ -446,14 +446,14 @@ class SubmissionFormWidget(QWidget):
class InfoItem(QWidget):
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> None:
run_object: BasicRun | None = None) -> None:
super().__init__(parent)
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value)
self.input: QWidget = self.set_widget(parent=parent, key=key, value=value, submission_type=submission_type,
sub_obj=sub_obj)
sub_obj=run_object)
self.setObjectName(key)
try:
self.missing: bool = value['missing']
@@ -492,7 +492,7 @@ class SubmissionFormWidget(QWidget):
def set_widget(self, parent: QWidget, key: str, value: dict,
submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> QWidget:
sub_obj: BasicRun | None = None) -> QWidget:
"""
Creates form widget
@@ -568,7 +568,7 @@ class SubmissionFormWidget(QWidget):
except ValueError:
categories.insert(0, categories.pop(categories.index(submission_type)))
add_widget.addItems(categories)
add_widget.setToolTip("Enter submission category or select from list.")
add_widget.setToolTip("Enter run category or select from list.")
case _:
if key in sub_obj.timestamps:
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
@@ -692,7 +692,7 @@ class SubmissionFormWidget(QWidget):
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
return wanted_reagent, report
else:
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
# NOTE: In this case we will have an empty reagent and the run will fail kit integrity check
return None, report
else:
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
@@ -801,7 +801,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
Report: Report on status of parse.
"""
report = Report()
logger.info(f"Hello from client submission form parser!")
logger.info(f"Hello from client run form parser!")
info = {}
reagents = []
for widget in self.findChildren(QWidget):

View File

@@ -43,7 +43,7 @@ class Summary(InfoPane):
orgs = self.org_select.get_checked()
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:
if self.report_obj.runs:
self.save_pdf_button.setEnabled(True)
self.save_excel_button.setEnabled(True)
else: