update to First Strand constructor

This commit is contained in:
Landon Wark
2023-10-12 13:09:12 -05:00
parent 1b6d415788
commit 957edb814a
13 changed files with 236 additions and 56 deletions

View File

@@ -1,3 +1,8 @@
## 202310.02
- Improvements to First strand constructor.
- Submission forms can now be dragged and dropped into the form widget.
## 202310.01 ## 202310.01
- Controls linker is now depreciated. - Controls linker is now depreciated.

View File

@@ -1,5 +1,5 @@
## Startup: ## Startup:
1. Open the app using the shortcut in the Submissions folder. For example: 'L:\Robotics Laboratory Support\Submissions\submissions_v122b.exe - Shortcut.lnk' (Version may have changed). 1. Open the app using the shortcut in the Submissions folder. For example: L:\\Robotics Laboratory Support\\Submissions\\submissions_v122b.exe - Shortcut.lnk (Version may have changed).
a. Ignore the large black window of fast scrolling text, it is there for debugging purposes. a. Ignore the large black window of fast scrolling text, it is there for debugging purposes.
b. The 'Submissions' tab should be open by default. b. The 'Submissions' tab should be open by default.
@@ -60,10 +60,6 @@ This is meant to import .xslx files created from the Design & Analysis Software
c. Repeat 6b for the Lot and the Expiry row and columns. c. Repeat 6b for the Lot and the Expiry row and columns.
7. Click the "Submit" button at the top. 7. Click the "Submit" button at the top.
## Linking Controls:
1. Click "Monthly" -> "Link Controls". Entire process should be handled automatically.
## Linking Extraction Logs: ## Linking Extraction Logs:
1. Click "Monthly" -> "Link Extraction Logs". 1. Click "Monthly" -> "Link Extraction Logs".

View File

@@ -1,6 +1,7 @@
- [x] Drag and drop files into submission form area?
- [ ] Get info for controls into their sample hitpicks. - [ ] Get info for controls into their sample hitpicks.
- [x] Move submission-type specific parser functions into class methods in their respective models. - [x] Move submission-type specific parser functions into class methods in their respective models.
- [ ] Improve results reporting. - [ ] Improve function results reporting.
- Maybe make it a list until it gets to the reporter? - Maybe make it a list until it gets to the reporter?
- [x] Increase robustness of form parsers by adding custom procedures for each. - [x] Increase robustness of form parsers by adding custom procedures for each.
- [x] Rerun Kit integrity if extraction kit changed in the form. - [x] Rerun Kit integrity if extraction kit changed in the form.

View File

@@ -56,8 +56,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# output_encoding = utf-8 # output_encoding = utf-8
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db ; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db ; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
[post_write_hooks] [post_write_hooks]

View File

@@ -4,7 +4,7 @@ from pathlib import Path
# Version of the realpython-reader package # Version of the realpython-reader package
__project__ = "submissions" __project__ = "submissions"
__version__ = "202310.1b" __version__ = "202310.2b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2023, Government of Canada" __copyright__ = "2022-2023, Government of Canada"

View File

@@ -417,7 +417,8 @@ def lookup_reagenttype_kittype_association(ctx:Settings,
def lookup_submission_sample_association(ctx:Settings, def lookup_submission_sample_association(ctx:Settings,
submission:models.BasicSubmission|str|None=None, submission:models.BasicSubmission|str|None=None,
sample:models.BasicSample|str|None=None, sample:models.BasicSample|str|None=None,
limit:int=0 limit:int=0,
chronologic:bool=False
) -> models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]: ) -> models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.SubmissionSampleAssociation) query = setup_lookup(ctx=ctx, locals=locals()).query(models.SubmissionSampleAssociation)
match submission: match submission:
@@ -435,6 +436,8 @@ def lookup_submission_sample_association(ctx:Settings,
case _: case _:
pass pass
logger.debug(f"Query count: {query.count()}") logger.debug(f"Query count: {query.count()}")
if chronologic:
query.join(models.BasicSubmission).order_by(models.BasicSubmission.submitted_date)
if query.count() == 1: if query.count() == 1:
limit = 1 limit = 1
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)

View File

@@ -247,6 +247,8 @@ def get_polymorphic_subclass(base:object, polymorphic_identity:str|None=None):
Returns: Returns:
_type_: Subclass, or parent class on _type_: Subclass, or parent class on
""" """
if isinstance(polymorphic_identity, dict):
polymorphic_identity = polymorphic_identity['value']
if polymorphic_identity == None: if polymorphic_identity == None:
return base return base
else: else:

View File

@@ -1,6 +1,7 @@
''' '''
Models for the main submission types. Models for the main submission types.
''' '''
from getpass import getuser
import math import math
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT, case from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT, case
@@ -15,7 +16,8 @@ from pandas import Timestamp
from dateutil.parser import parse from dateutil.parser import parse
import re import re
import pandas as pd import pandas as pd
from tools import row_map from openpyxl import Workbook
from tools import check_not_nan, row_map
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -238,6 +240,20 @@ class BasicSubmission(Base):
continue continue
return output_list return output_list
@classmethod
def custom_platemap(cls, xl:pd.ExcelFile, plate_map:pd.DataFrame) -> pd.DataFrame:
"""
Stupid stopgap solution to there being an issue with the Bacterial Culture plate map
Args:
xl (pd.ExcelFile): original xl workbook
plate_map (pd.DataFrame): original plate map
Returns:
pd.DataFrame: updated plate map.
"""
return plate_map
@classmethod @classmethod
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict: def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
""" """
@@ -266,6 +282,19 @@ class BasicSubmission(Base):
logger.debug(f"Called {cls.__name__} sample parser") logger.debug(f"Called {cls.__name__} sample parser")
return input_dict return input_dict
@classmethod
def custom_autofill(cls, input_excel:Workbook) -> Workbook:
"""
Adds custom autofill methods for submission
Args:
input_excel (Workbook): input workbook
Returns:
Workbook: updated workbook
"""
return input_excel
# Below are the custom submission types # Below are the custom submission types
class BacterialCulture(BasicSubmission): class BacterialCulture(BasicSubmission):
@@ -287,6 +316,49 @@ class BacterialCulture(BasicSubmission):
output['controls'] = [item.to_sub_dict() for item in self.controls] output['controls'] = [item.to_sub_dict() for item in self.controls]
return output return output
@classmethod
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
"""
Stupid stopgap solution to there being an issue with the Bacterial Culture plate map. Extends parent.
Args:
xl (pd.ExcelFile): original xl workbook
plate_map (pd.DataFrame): original plate map
Returns:
pd.DataFrame: updated plate map.
"""
plate_map = super().custom_platemap(xl, plate_map)
num1 = xl.parse("Sample List").iloc[40,1]
num2 = xl.parse("Sample List").iloc[41,1]
logger.debug(f"Broken: {plate_map.iloc[5,0]}, {plate_map.iloc[6,0]}")
logger.debug(f"Replace: {num1}, {num2}")
if not check_not_nan(plate_map.iloc[5,0]):
plate_map.iloc[5,0] = num1
if not check_not_nan(plate_map.iloc[6,0]):
plate_map.iloc[6,0] = num2
return plate_map
@classmethod
def custom_autofill(cls, input_excel: Workbook) -> Workbook:
"""
Stupid stopgap solution to there being an issue with the Bacterial Culture plate map. Extends parent.
Args:
input_excel (Workbook): Input openpyxl workbook
Returns:
Workbook: Updated openpyxl workbook
"""
input_excel = super().custom_autofill(input_excel)
sheet = input_excel['Plate Map']
if sheet.cell(12,2).value == None:
sheet.cell(row=12, column=2, value="=IF(ISBLANK('Sample List'!$B42),\"\",'Sample List'!$B42)")
if sheet.cell(13,2).value == None:
sheet.cell(row=13, column=2, value="=IF(ISBLANK('Sample List'!$B43),\"\",'Sample List'!$B43)")
input_excel["Sample List"].cell(row=15, column=2, value=getuser()[0:2].upper())
return input_excel
class Wastewater(BasicSubmission): class Wastewater(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
@@ -327,7 +399,6 @@ class Wastewater(BasicSubmission):
input_dict['csv'] = xl.parse("Copy to import file") input_dict['csv'] = xl.parse("Copy to import file")
return input_dict return input_dict
class WastewaterArtic(BasicSubmission): class WastewaterArtic(BasicSubmission):
""" """
derivative submission type for artic wastewater derivative submission type for artic wastewater
@@ -371,7 +442,6 @@ class WastewaterArtic(BasicSubmission):
input_dict['submitter_id'] = re.sub(r"\s\(.+\)$", "", str(input_dict['submitter_id'])).strip() input_dict['submitter_id'] = re.sub(r"\s\(.+\)$", "", str(input_dict['submitter_id'])).strip()
return input_dict return input_dict
class BasicSample(Base): class BasicSample(Base):
""" """
Base of basic sample which polymorphs into BCSample and WWSample Base of basic sample which polymorphs into BCSample and WWSample
@@ -457,7 +527,7 @@ class BasicSample(Base):
Sample name: {self.submitter_id}<br> Sample name: {self.submitter_id}<br>
Well: {row_map[assoc.row]}{assoc.column} Well: {row_map[assoc.row]}{assoc.column}
""" """
return dict(name=self.submitter_id, positive=False, tooltip=tooltip_text) return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
class WastewaterSample(BasicSample): class WastewaterSample(BasicSample):
""" """
@@ -539,6 +609,16 @@ class WastewaterSample(BasicSample):
logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.") logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
return sample return sample
def get_recent_ww_submission(self):
results = [sub for sub in self.submissions if isinstance(sub, Wastewater)]
if len(results) > 1:
results = results.sort(key=lambda sub: sub.submitted_date)
try:
return results[0]
except IndexError:
return None
class BacterialCultureSample(BasicSample): class BacterialCultureSample(BasicSample):
""" """
base of bacterial culture sample base of bacterial culture sample

View File

@@ -376,6 +376,8 @@ class SampleParser(object):
df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']] df = df.iloc[plate_map_location['start_row']-1:plate_map_location['end_row'], plate_map_location['start_column']-1:plate_map_location['end_column']]
df = pd.DataFrame(df.values[1:], columns=df.iloc[0]) df = pd.DataFrame(df.values[1:], columns=df.iloc[0])
df = df.set_index(df.columns[0]) df = df.set_index(df.columns[0])
custom_mapper = get_polymorphic_subclass(models.BasicSubmission, self.submission_type)
df = custom_mapper.custom_platemap(self.xl, df)
return df return df
def construct_lookup_table(self, lookup_table_location:dict) -> pd.DataFrame: def construct_lookup_table(self, lookup_table_location:dict) -> pd.DataFrame:

View File

@@ -8,17 +8,15 @@ from PyQt6.QtWidgets import (
QMainWindow, QToolBar, QMainWindow, QToolBar,
QTabWidget, QWidget, QVBoxLayout, QTabWidget, QWidget, QVBoxLayout,
QComboBox, QHBoxLayout, QComboBox, QHBoxLayout,
QScrollArea, QLineEdit, QDateEdit, QScrollArea, QLineEdit, QDateEdit
QSpinBox
) )
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from pathlib import Path from pathlib import Path
from backend.db import ( from backend.db import (
construct_reagent, store_object, lookup_control_types, lookup_modes construct_reagent, store_object, lookup_control_types, lookup_modes
) )
# from .all_window_functions import extract_form_info
from tools import check_if_app, Settings from tools import check_if_app, Settings
from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker, ImportReagent from frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker, ImportReagent
import logging import logging
@@ -55,7 +53,6 @@ class App(QMainWindow):
self._createToolBar() self._createToolBar()
self._connectActions() self._connectActions()
self._controls_getter() self._controls_getter()
# self.status_bar = self.statusBar()
self.show() self.show()
self.statusBar().showMessage('Ready', 5000) self.statusBar().showMessage('Ready', 5000)
@@ -68,7 +65,6 @@ class App(QMainWindow):
menuBar = self.menuBar() menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File") fileMenu = menuBar.addMenu("&File")
# Creating menus using a title # Creating menus using a title
# editMenu = menuBar.addMenu("&Edit")
methodsMenu = menuBar.addMenu("&Methods") methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports") reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly") maintenanceMenu = menuBar.addMenu("&Monthly")
@@ -79,7 +75,6 @@ class App(QMainWindow):
fileMenu.addAction(self.importPCRAction) fileMenu.addAction(self.importPCRAction)
methodsMenu.addAction(self.constructFS) methodsMenu.addAction(self.constructFS)
reportMenu.addAction(self.generateReportAction) reportMenu.addAction(self.generateReportAction)
# maintenanceMenu.addAction(self.joinControlsAction)
maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction) maintenanceMenu.addAction(self.joinPCRAction)
@@ -105,7 +100,6 @@ class App(QMainWindow):
self.generateReportAction = QAction("Make Report", self) self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self) self.addKitAction = QAction("Import Kit", self)
self.addOrgAction = QAction("Import Org", self) self.addOrgAction = QAction("Import Org", self)
# self.joinControlsAction = QAction("Link Controls")
self.joinExtractionAction = QAction("Link Extraction Logs") self.joinExtractionAction = QAction("Link Extraction Logs")
self.joinPCRAction = QAction("Link PCR Logs") self.joinPCRAction = QAction("Link PCR Logs")
self.helpAction = QAction("&About", self) self.helpAction = QAction("&About", self)
@@ -128,12 +122,12 @@ class App(QMainWindow):
self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter) self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter)
self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter) self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter)
self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter) self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
# self.joinControlsAction.triggered.connect(self.linkControls)
self.joinExtractionAction.triggered.connect(self.linkExtractions) self.joinExtractionAction.triggered.connect(self.linkExtractions)
self.joinPCRAction.triggered.connect(self.linkPCR) self.joinPCRAction.triggered.connect(self.linkPCR)
self.helpAction.triggered.connect(self.showAbout) self.helpAction.triggered.connect(self.showAbout)
self.docsAction.triggered.connect(self.openDocs) self.docsAction.triggered.connect(self.openDocs)
self.constructFS.triggered.connect(self.construct_first_strand) self.constructFS.triggered.connect(self.construct_first_strand)
self.table_widget.formwidget.import_drag.connect(self.importSubmission)
def showAbout(self): def showAbout(self):
""" """
@@ -168,12 +162,14 @@ class App(QMainWindow):
else: else:
self.statusBar().showMessage("Action completed sucessfully.", 5000) self.statusBar().showMessage("Action completed sucessfully.", 5000)
def importSubmission(self): def importSubmission(self, fname:Path|None=None):
""" """
import submission from excel sheet into form import submission from excel sheet into form
""" """
from .main_window_functions import import_submission_function from .main_window_functions import import_submission_function
self, result = import_submission_function(self) self.raise_()
self.activateWindow()
self, result = import_submission_function(self, fname)
logger.debug(f"Import result: {result}") logger.debug(f"Import result: {result}")
self.result_reporter(result) self.result_reporter(result)
@@ -395,12 +391,25 @@ class AddSubForm(QWidget):
class SubmissionFormWidget(QWidget): class SubmissionFormWidget(QWidget):
import_drag = pyqtSignal(Path)
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
logger.debug(f"Setting form widget...") logger.debug(f"Setting form widget...")
super().__init__(parent) super().__init__(parent)
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer", self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
"qt_scrollarea_vcontainer", "submit_btn" "qt_scrollarea_vcontainer", "submit_btn"
] ]
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
fname = Path([u.toLocalFile() for u in event.mimeData().urls()][0])
self.import_drag.emit(fname)
def parse_form(self) -> Tuple[dict, list]: def parse_form(self) -> Tuple[dict, list]:
logger.debug(f"Hello from parser!") logger.debug(f"Hello from parser!")

View File

@@ -3,17 +3,19 @@ Contains miscellaneous widgets for frontend functions
''' '''
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
from PyQt6 import QtCore
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog, QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget, QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox, QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
QHBoxLayout, QScrollArea QHBoxLayout, QScrollArea, QFormLayout
) )
from PyQt6.QtCore import Qt, QDate, QSize from PyQt6.QtCore import Qt, QDate, QSize
from tools import check_not_nan, jinja_template_loading, Settings from tools import check_not_nan, jinja_template_loading, Settings
from backend.db.functions import construct_kit_from_yaml, \ from backend.db.functions import construct_kit_from_yaml, \
lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association lookup_reagent_types, lookup_reagents, lookup_submission_type, lookup_reagenttype_kittype_association, \
lookup_submissions
from backend.db.models import SubmissionTypeKitTypeAssociation from backend.db.models import SubmissionTypeKitTypeAssociation
from sqlalchemy import FLOAT, INTEGER, String from sqlalchemy import FLOAT, INTEGER, String
import logging import logging
@@ -277,7 +279,6 @@ class KitAdder(QWidget):
info[widget.objectName()] = widget.date().toPyDate() info[widget.objectName()] = widget.date().toPyDate()
return info, reagents return info, reagents
class ReagentTypeForm(QWidget): class ReagentTypeForm(QWidget):
""" """
custom widget to add information about a new reagenttype custom widget to add information about a new reagenttype
@@ -458,3 +459,67 @@ class ParsedQLabel(QLabel):
else: else:
self.setText(f"MISSING {output}") self.setText(f"MISSING {output}")
class FirstStrandSalvage(QDialog):
def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None:
super().__init__()
if rsl_plate_num == None:
rsl_plate_num = ""
self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.submitter_id_input = QLineEdit()
self.submitter_id_input.setText(submitter_id)
self.rsl_plate_num = QLineEdit()
self.rsl_plate_num.setText(rsl_plate_num)
self.row_letter = QComboBox()
self.row_letter.addItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
self.row_letter.setEditable(False)
self.column_number = QComboBox()
self.column_number.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'])
self.column_number.setEditable(False)
self.layout = QFormLayout()
self.layout.addRow(self.tr("&Sample Number:"), self.submitter_id_input)
self.layout.addRow(self.tr("&Plate Number:"), self.rsl_plate_num)
self.layout.addRow(self.tr("&Source Row:"), self.row_letter)
self.layout.addRow(self.tr("&Source Column:"), self.column_number)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}")
class FirstStrandPlateList(QDialog):
def __init__(self, ctx:Settings) -> None:
super().__init__()
self.setWindowTitle("First Strand Plates")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
ww = [item.rsl_plate_num for item in lookup_submissions(ctx=ctx, submission_type="Wastewater")]
self.plate1 = QComboBox()
self.plate2 = QComboBox()
self.plate3 = QComboBox()
self.layout = QFormLayout()
for ii, plate in enumerate([self.plate1, self.plate2, self.plate3]):
plate.addItems(ww)
self.layout.addRow(self.tr(f"&Plate {ii+1}:"), plate)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self):
output = []
for plate in [self.plate1, self.plate2, self.plate3]:
output.append(plate.currentText())
return output

View File

@@ -291,8 +291,6 @@ class SubmissionDetails(QDialog):
btn.setParent(self) btn.setParent(self)
btn.setFixedWidth(900) btn.setFixedWidth(900)
btn.clicked.connect(self.export) btn.clicked.connect(self.export)
with open("test.html", "w") as f:
f.write(self.html)
def export(self): def export(self):
""" """

View File

@@ -22,10 +22,12 @@ from PyQt6.QtWidgets import (
) )
from .all_window_functions import select_open_file, select_save_file from .all_window_functions import select_open_file, select_save_file
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from backend.db.models import BasicSubmission
from backend.db.functions import ( from backend.db.functions import (
construct_submission_info, lookup_reagents, construct_kit_from_yaml, construct_org_from_yaml, get_control_subtypes, construct_submission_info, lookup_reagents, construct_kit_from_yaml, construct_org_from_yaml, get_control_subtypes,
update_subsampassoc_with_pcr, check_kit_integrity, update_last_used, lookup_organizations, lookup_kit_types, update_subsampassoc_with_pcr, check_kit_integrity, update_last_used, lookup_organizations, lookup_kit_types,
lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object, lookup_submission_type lookup_submissions, lookup_controls, lookup_samples, lookup_submission_sample_association, store_object, lookup_submission_type,
get_polymorphic_subclass
) )
from backend.excel.parser import SheetParser, PCRParser, SampleParser from backend.excel.parser import SheetParser, PCRParser, SampleParser
from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df from backend.excel.reports import make_report_html, make_report_xlsx, convert_data_list_to_df
@@ -34,10 +36,12 @@ from .custom_widgets.pop_ups import AlertPop, QuestionAsker
from .custom_widgets import ReportDatePicker from .custom_widgets import ReportDatePicker
from .custom_widgets.misc import ImportReagent, ParsedQLabel from .custom_widgets.misc import ImportReagent, ParsedQLabel
from .visualizations.control_charts import create_charts, construct_html from .visualizations.control_charts import create_charts, construct_html
from pathlib import Path
from frontend.custom_widgets.misc import FirstStrandSalvage, FirstStrandPlateList
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]: def import_submission_function(obj:QMainWindow, fname:Path|None=None) -> Tuple[QMainWindow, dict|None]:
""" """
Import a new submission to the app window Import a new submission to the app window
@@ -56,6 +60,7 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
obj.missing_info = [] obj.missing_info = []
# set file dialog # set file dialog
if isinstance(fname, bool) or fname == None:
fname = select_open_file(obj, file_extension="xlsx") fname = select_open_file(obj, file_extension="xlsx")
logger.debug(f"Attempting to parse file: {fname}") logger.debug(f"Attempting to parse file: {fname}")
if not fname.exists(): if not fname.exists():
@@ -113,7 +118,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
add_widget = QComboBox() add_widget = QComboBox()
# lookup existing kits by 'submission_type' decided on by sheetparser # lookup existing kits by 'submission_type' decided on by sheetparser
logger.debug(f"Looking up kits used for {pyd.submission_type['value']}") logger.debug(f"Looking up kits used for {pyd.submission_type['value']}")
# uses = [item.__str__() for item in lookup_kittype_by_use(ctx=obj.ctx, used_for=pyd.submission_type['value'])]
uses = [item.__str__() for item in lookup_kit_types(ctx=obj.ctx, used_for=pyd.submission_type['value'])] uses = [item.__str__() for item in lookup_kit_types(ctx=obj.ctx, used_for=pyd.submission_type['value'])]
logger.debug(f"Kits received for {pyd.submission_type['value']}: {uses}") logger.debug(f"Kits received for {pyd.submission_type['value']}: {uses}")
if check_not_nan(value['value']): if check_not_nan(value['value']):
@@ -125,7 +129,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
obj.ext_kit = uses[0] obj.ext_kit = uses[0]
# Run reagent scraper whenever extraction kit is changed. # Run reagent scraper whenever extraction kit is changed.
add_widget.currentTextChanged.connect(obj.scrape_reagents) add_widget.currentTextChanged.connect(obj.scrape_reagents)
# add_widget.addItems(uses)
case 'submitted_date': case 'submitted_date':
# uses base calendar # uses base calendar
add_widget = QDateEdit(calendarPopup=True) add_widget = QDateEdit(calendarPopup=True)
@@ -221,7 +224,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
# get current kit being used # get current kit being used
obj.ext_kit = kit_widget.currentText() obj.ext_kit = kit_widget.currentText()
for item in obj.reagents: for item in obj.reagents:
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False)) obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':True}, item.type, title=False, label_name=f"lot_{item.type}"))
reagent = dict(type=item.type, lot=item.lot, exp=item.exp, name=item.name) reagent = dict(type=item.type, lot=item.lot, exp=item.exp, name=item.name)
add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit) add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit)
obj.table_widget.formlayout.addWidget(add_widget) obj.table_widget.formlayout.addWidget(add_widget)
@@ -231,7 +234,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
result = dict(message=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in obj.missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning") result = dict(message=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in obj.missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning")
for item in obj.missing_reagents: for item in obj.missing_reagents:
# Add label that has parsed as False to show "MISSING" label. # Add label that has parsed as False to show "MISSING" label.
obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False)) obj.table_widget.formlayout.addWidget(ParsedQLabel({'parsed':False}, item.type, title=False, label_name=f"missing_{item.type}"))
# Set default parameters for the empty reagent. # Set default parameters for the empty reagent.
reagent = dict(type=item.type, lot=None, exp=date.today(), name=None) reagent = dict(type=item.type, lot=None, exp=date.today(), name=None)
# create and add widget # create and add widget
@@ -896,8 +899,8 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
logger.debug(f"Attempting: {item['type']}") logger.debug(f"Attempting: {item['type']}")
worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value']) worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value'])
# Hacky way to pop in 'signed by' # Hacky way to pop in 'signed by'
if info['submission_type'] == "Bacterial Culture": custom_parser = get_polymorphic_subclass(BasicSubmission, info['submission_type'])
workbook["Sample List"].cell(row=14, column=2, value=getuser()[0:2].upper()) workbook = custom_parser.custom_autofill(workbook)
fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx") fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx")
workbook.save(filename=fname.__str__()) workbook.save(filename=fname.__str__())
@@ -934,46 +937,63 @@ def construct_first_strand_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]
logger.debug(f"Samples: {pformat(samples)}") logger.debug(f"Samples: {pformat(samples)}")
logger.debug("Called first strand sample parser") logger.debug("Called first strand sample parser")
plates = sprsr.grab_plates() plates = sprsr.grab_plates()
# Fix no plates found in form.
if plates == []:
dlg = FirstStrandPlateList(ctx=obj.ctx)
if dlg.exec():
plates = dlg.parse_form()
plates = list(set(plates))
logger.debug(f"Plates: {pformat(plates)}") logger.debug(f"Plates: {pformat(plates)}")
output_samples = [] output_samples = []
logger.debug(f"Samples: {pformat(samples)}") logger.debug(f"Samples: {pformat(samples)}")
old_plate_number = 1 old_plate_number = 1
old_plate = ''
for item in samples: for item in samples:
try: try:
item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0] item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0]
except AttributeError: except AttributeError:
item['well'] = item pass
item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip() item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip()
new_dict = {} new_dict = {}
new_dict['sample'] = item['submitter_id'] new_dict['sample'] = item['submitter_id']
if item['submitter_id'] == "NTC1":
new_dict['destination_row'] = 8
new_dict['destination_column'] = 2
new_dict['plate_number'] = 'control'
elif item['submitter_id'] == "NTC2":
new_dict['destination_row'] = 8
new_dict['destination_column'] = 5
new_dict['plate_number'] = 'control'
else:
new_dict['destination_row'] = item['row']
new_dict['destination_column'] = item['column']
plate_num, plate = get_plates(input_sample_number=new_dict['sample'], plates=plates) plate_num, plate = get_plates(input_sample_number=new_dict['sample'], plates=plates)
if plate_num == None: if plate_num == None:
plate_num = str(old_plate_number) + "*" plate_num = str(old_plate_number) + "*"
else: else:
old_plate_number = plate_num old_plate_number = plate_num
logger.debug(f"Got plate number: {plate_num}, plate: {plate}") logger.debug(f"Got plate number: {plate_num}, plate: {plate}")
if item['submitter_id'] == "NTC1":
new_dict['destination_row'] = 8
new_dict['destination_column'] = 2
new_dict['plate_number'] = 'control'
new_dict['plate'] = None
output_samples.append(new_dict)
continue
elif item['submitter_id'] == "NTC2":
new_dict['destination_row'] = 8
new_dict['destination_column'] = 5
new_dict['plate_number'] = 'control'
new_dict['plate'] = None
output_samples.append(new_dict)
continue
else:
new_dict['destination_row'] = item['row']
new_dict['destination_column'] = item['column']
new_dict['plate_number'] = plate_num
# Fix plate association not found
if plate == None: if plate == None:
dlg = FirstStrandSalvage(ctx=obj.ctx, submitter_id=item['submitter_id'], rsl_plate_num=old_plate)
if dlg.exec():
item.update(dlg.parse_form())
try: try:
new_dict['source_row'], new_dict['source_column'] = convert_well_to_row_column(item['well']) new_dict['source_row'], new_dict['source_column'] = convert_well_to_row_column(item['well'])
new_dict['plate_number'] = plate_num
except KeyError: except KeyError:
pass pass
else: else:
new_dict['plate_number'] = plate_num
new_dict['plate'] = plate.submission.rsl_plate_num new_dict['plate'] = plate.submission.rsl_plate_num
new_dict['source_row'] = plate.row new_dict['source_row'] = plate.row
new_dict['source_column'] = plate.column new_dict['source_column'] = plate.column
old_plate = plate.submission.rsl_plate_num
output_samples.append(new_dict) output_samples.append(new_dict)
df = pd.DataFrame.from_records(output_samples) df = pd.DataFrame.from_records(output_samples)
df.sort_values(by=['destination_column', 'destination_row'], ascending=True, inplace=True) df.sort_values(by=['destination_column', 'destination_row'], ascending=True, inplace=True)
@@ -1001,6 +1021,7 @@ def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, d
obj.missing_reagents = [] obj.missing_reagents = []
# Remove previous reagent widgets # Remove previous reagent widgets
[item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget) if item.objectName().startswith("lot_") or item.objectName().startswith("missing_")] [item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QWidget) if item.objectName().startswith("lot_") or item.objectName().startswith("missing_")]
[item.setParent(None) for item in obj.table_widget.formlayout.parentWidget().findChildren(QPushButton)]
reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit) reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
logger.debug(f"Got reagents: {reagents}") logger.debug(f"Got reagents: {reagents}")
for reagent in obj.prsr.sub['reagents']: for reagent in obj.prsr.sub['reagents']:
@@ -1013,5 +1034,3 @@ def scrape_reagents(obj:QMainWindow, extraction_kit:str) -> Tuple[QMainWindow, d
logger.debug(f"Missing reagents: {obj.missing_reagents}") logger.debug(f"Missing reagents: {obj.missing_reagents}")
return obj, None return obj, None