update to First Strand constructor
This commit is contained in:
@@ -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
|
||||
|
||||
- Controls linker is now depreciated.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
## 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.
|
||||
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.
|
||||
7. Click the "Submit" button at the top.
|
||||
|
||||
## Linking Controls:
|
||||
|
||||
1. Click "Monthly" -> "Link Controls". Entire process should be handled automatically.
|
||||
|
||||
## Linking Extraction Logs:
|
||||
|
||||
1. Click "Monthly" -> "Link Extraction Logs".
|
||||
|
||||
3
TODO.md
3
TODO.md
@@ -1,6 +1,7 @@
|
||||
- [x] Drag and drop files into submission form area?
|
||||
- [ ] Get info for controls into their sample hitpicks.
|
||||
- [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?
|
||||
- [x] Increase robustness of form parsers by adding custom procedures for each.
|
||||
- [x] Rerun Kit integrity if extraction kit changed in the form.
|
||||
|
||||
@@ -56,8 +56,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
||||
# output_encoding = utf-8
|
||||
|
||||
; 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\python\submissions\tests\test_assets\submissions-test.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
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
# Version of the realpython-reader package
|
||||
__project__ = "submissions"
|
||||
__version__ = "202310.1b"
|
||||
__version__ = "202310.2b"
|
||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||
__copyright__ = "2022-2023, Government of Canada"
|
||||
|
||||
|
||||
@@ -417,7 +417,8 @@ def lookup_reagenttype_kittype_association(ctx:Settings,
|
||||
def lookup_submission_sample_association(ctx:Settings,
|
||||
submission:models.BasicSubmission|str|None=None,
|
||||
sample:models.BasicSample|str|None=None,
|
||||
limit:int=0
|
||||
limit:int=0,
|
||||
chronologic:bool=False
|
||||
) -> models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]:
|
||||
query = setup_lookup(ctx=ctx, locals=locals()).query(models.SubmissionSampleAssociation)
|
||||
match submission:
|
||||
@@ -435,6 +436,8 @@ def lookup_submission_sample_association(ctx:Settings,
|
||||
case _:
|
||||
pass
|
||||
logger.debug(f"Query count: {query.count()}")
|
||||
if chronologic:
|
||||
query.join(models.BasicSubmission).order_by(models.BasicSubmission.submitted_date)
|
||||
if query.count() == 1:
|
||||
limit = 1
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
@@ -247,6 +247,8 @@ def get_polymorphic_subclass(base:object, polymorphic_identity:str|None=None):
|
||||
Returns:
|
||||
_type_: Subclass, or parent class on
|
||||
"""
|
||||
if isinstance(polymorphic_identity, dict):
|
||||
polymorphic_identity = polymorphic_identity['value']
|
||||
if polymorphic_identity == None:
|
||||
return base
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'''
|
||||
Models for the main submission types.
|
||||
'''
|
||||
from getpass import getuser
|
||||
import math
|
||||
from . import Base
|
||||
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
|
||||
import re
|
||||
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__}")
|
||||
|
||||
@@ -238,6 +240,20 @@ class BasicSubmission(Base):
|
||||
continue
|
||||
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
|
||||
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||
"""
|
||||
@@ -265,6 +281,19 @@ class BasicSubmission(Base):
|
||||
"""
|
||||
logger.debug(f"Called {cls.__name__} sample parser")
|
||||
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
|
||||
|
||||
@@ -286,6 +315,49 @@ class BacterialCulture(BasicSubmission):
|
||||
if full_data:
|
||||
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
||||
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):
|
||||
"""
|
||||
@@ -326,8 +398,7 @@ class Wastewater(BasicSubmission):
|
||||
if xl != None:
|
||||
input_dict['csv'] = xl.parse("Copy to import file")
|
||||
return input_dict
|
||||
|
||||
|
||||
|
||||
class WastewaterArtic(BasicSubmission):
|
||||
"""
|
||||
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()
|
||||
return input_dict
|
||||
|
||||
|
||||
class BasicSample(Base):
|
||||
"""
|
||||
Base of basic sample which polymorphs into BCSample and WWSample
|
||||
@@ -457,7 +527,7 @@ class BasicSample(Base):
|
||||
Sample name: {self.submitter_id}<br>
|
||||
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):
|
||||
"""
|
||||
@@ -538,6 +608,16 @@ class WastewaterSample(BasicSample):
|
||||
except (TypeError, AttributeError) as e:
|
||||
logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -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 = pd.DataFrame(df.values[1:], columns=df.iloc[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
|
||||
|
||||
def construct_lookup_table(self, lookup_table_location:dict) -> pd.DataFrame:
|
||||
|
||||
@@ -8,17 +8,15 @@ from PyQt6.QtWidgets import (
|
||||
QMainWindow, QToolBar,
|
||||
QTabWidget, QWidget, QVBoxLayout,
|
||||
QComboBox, QHBoxLayout,
|
||||
QScrollArea, QLineEdit, QDateEdit,
|
||||
QSpinBox
|
||||
QScrollArea, QLineEdit, QDateEdit
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from pathlib import Path
|
||||
from backend.db import (
|
||||
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 frontend.custom_widgets import SubmissionsSheet, AlertPop, AddReagentForm, KitAdder, ControlsDatePicker, ImportReagent
|
||||
import logging
|
||||
@@ -55,7 +53,6 @@ class App(QMainWindow):
|
||||
self._createToolBar()
|
||||
self._connectActions()
|
||||
self._controls_getter()
|
||||
# self.status_bar = self.statusBar()
|
||||
self.show()
|
||||
self.statusBar().showMessage('Ready', 5000)
|
||||
|
||||
@@ -68,7 +65,6 @@ class App(QMainWindow):
|
||||
menuBar = self.menuBar()
|
||||
fileMenu = menuBar.addMenu("&File")
|
||||
# Creating menus using a title
|
||||
# editMenu = menuBar.addMenu("&Edit")
|
||||
methodsMenu = menuBar.addMenu("&Methods")
|
||||
reportMenu = menuBar.addMenu("&Reports")
|
||||
maintenanceMenu = menuBar.addMenu("&Monthly")
|
||||
@@ -79,7 +75,6 @@ class App(QMainWindow):
|
||||
fileMenu.addAction(self.importPCRAction)
|
||||
methodsMenu.addAction(self.constructFS)
|
||||
reportMenu.addAction(self.generateReportAction)
|
||||
# maintenanceMenu.addAction(self.joinControlsAction)
|
||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||
maintenanceMenu.addAction(self.joinPCRAction)
|
||||
|
||||
@@ -105,7 +100,6 @@ class App(QMainWindow):
|
||||
self.generateReportAction = QAction("Make Report", self)
|
||||
self.addKitAction = QAction("Import Kit", self)
|
||||
self.addOrgAction = QAction("Import Org", self)
|
||||
# self.joinControlsAction = QAction("Link Controls")
|
||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||
self.joinPCRAction = QAction("Link PCR Logs")
|
||||
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.datepicker.start_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.joinPCRAction.triggered.connect(self.linkPCR)
|
||||
self.helpAction.triggered.connect(self.showAbout)
|
||||
self.docsAction.triggered.connect(self.openDocs)
|
||||
self.constructFS.triggered.connect(self.construct_first_strand)
|
||||
self.table_widget.formwidget.import_drag.connect(self.importSubmission)
|
||||
|
||||
def showAbout(self):
|
||||
"""
|
||||
@@ -168,12 +162,14 @@ class App(QMainWindow):
|
||||
else:
|
||||
self.statusBar().showMessage("Action completed sucessfully.", 5000)
|
||||
|
||||
def importSubmission(self):
|
||||
def importSubmission(self, fname:Path|None=None):
|
||||
"""
|
||||
import submission from excel sheet into form
|
||||
"""
|
||||
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}")
|
||||
self.result_reporter(result)
|
||||
|
||||
@@ -395,12 +391,25 @@ class AddSubForm(QWidget):
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
|
||||
import_drag = pyqtSignal(Path)
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
logger.debug(f"Setting form widget...")
|
||||
super().__init__(parent)
|
||||
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
|
||||
"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]:
|
||||
logger.debug(f"Hello from parser!")
|
||||
|
||||
@@ -3,17 +3,19 @@ Contains miscellaneous widgets for frontend functions
|
||||
'''
|
||||
from datetime import date
|
||||
from pprint import pformat
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout,
|
||||
QLineEdit, QComboBox, QDialog,
|
||||
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
|
||||
QGridLayout, QPushButton, QSpinBox, QDoubleSpinBox,
|
||||
QHBoxLayout, QScrollArea
|
||||
QHBoxLayout, QScrollArea, QFormLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QDate, QSize
|
||||
from tools import check_not_nan, jinja_template_loading, Settings
|
||||
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 sqlalchemy import FLOAT, INTEGER, String
|
||||
import logging
|
||||
@@ -277,7 +279,6 @@ class KitAdder(QWidget):
|
||||
info[widget.objectName()] = widget.date().toPyDate()
|
||||
return info, reagents
|
||||
|
||||
|
||||
class ReagentTypeForm(QWidget):
|
||||
"""
|
||||
custom widget to add information about a new reagenttype
|
||||
@@ -458,3 +459,67 @@ class ParsedQLabel(QLabel):
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -291,8 +291,6 @@ class SubmissionDetails(QDialog):
|
||||
btn.setParent(self)
|
||||
btn.setFixedWidth(900)
|
||||
btn.clicked.connect(self.export)
|
||||
with open("test.html", "w") as f:
|
||||
f.write(self.html)
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
|
||||
@@ -22,10 +22,12 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
from .all_window_functions import select_open_file, select_save_file
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend.db.models import BasicSubmission
|
||||
from backend.db.functions import (
|
||||
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,
|
||||
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.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.misc import ImportReagent, ParsedQLabel
|
||||
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__}")
|
||||
|
||||
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
|
||||
|
||||
@@ -56,7 +60,8 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
|
||||
obj.missing_info = []
|
||||
|
||||
# set file dialog
|
||||
fname = select_open_file(obj, file_extension="xlsx")
|
||||
if isinstance(fname, bool) or fname == None:
|
||||
fname = select_open_file(obj, file_extension="xlsx")
|
||||
logger.debug(f"Attempting to parse file: {fname}")
|
||||
if not fname.exists():
|
||||
result = dict(message=f"File {fname.__str__()} not found.", status="critical")
|
||||
@@ -113,7 +118,6 @@ def import_submission_function(obj:QMainWindow) -> Tuple[QMainWindow, dict|None]
|
||||
add_widget = QComboBox()
|
||||
# lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
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'])]
|
||||
logger.debug(f"Kits received for {pyd.submission_type['value']}: {uses}")
|
||||
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]
|
||||
# Run reagent scraper whenever extraction kit is changed.
|
||||
add_widget.currentTextChanged.connect(obj.scrape_reagents)
|
||||
# add_widget.addItems(uses)
|
||||
case 'submitted_date':
|
||||
# uses base calendar
|
||||
add_widget = QDateEdit(calendarPopup=True)
|
||||
@@ -221,7 +224,7 @@ def kit_integrity_completion_function(obj:QMainWindow) -> Tuple[QMainWindow, dic
|
||||
# get current kit being used
|
||||
obj.ext_kit = kit_widget.currentText()
|
||||
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)
|
||||
add_widget = ImportReagent(ctx=obj.ctx, reagent=reagent, extraction_kit=obj.ext_kit)
|
||||
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")
|
||||
for item in obj.missing_reagents:
|
||||
# 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.
|
||||
reagent = dict(type=item.type, lot=None, exp=date.today(), name=None)
|
||||
# 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']}")
|
||||
worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value'])
|
||||
# Hacky way to pop in 'signed by'
|
||||
if info['submission_type'] == "Bacterial Culture":
|
||||
workbook["Sample List"].cell(row=14, column=2, value=getuser()[0:2].upper())
|
||||
custom_parser = get_polymorphic_subclass(BasicSubmission, info['submission_type'])
|
||||
workbook = custom_parser.custom_autofill(workbook)
|
||||
fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx")
|
||||
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("Called first strand sample parser")
|
||||
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)}")
|
||||
output_samples = []
|
||||
logger.debug(f"Samples: {pformat(samples)}")
|
||||
old_plate_number = 1
|
||||
old_plate = ''
|
||||
for item in samples:
|
||||
try:
|
||||
item['well'] = re.search(r"\s\((.*)\)$", item['submitter_id']).groups()[0]
|
||||
except AttributeError:
|
||||
item['well'] = item
|
||||
pass
|
||||
item['submitter_id'] = re.sub(r"\s\(.*\)$", "", str(item['submitter_id'])).strip()
|
||||
new_dict = {}
|
||||
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)
|
||||
if plate_num == None:
|
||||
plate_num = str(old_plate_number) + "*"
|
||||
else:
|
||||
old_plate_number = plate_num
|
||||
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:
|
||||
dlg = FirstStrandSalvage(ctx=obj.ctx, submitter_id=item['submitter_id'], rsl_plate_num=old_plate)
|
||||
if dlg.exec():
|
||||
item.update(dlg.parse_form())
|
||||
try:
|
||||
new_dict['source_row'], new_dict['source_column'] = convert_well_to_row_column(item['well'])
|
||||
new_dict['plate_number'] = plate_num
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
new_dict['plate_number'] = plate_num
|
||||
new_dict['plate'] = plate.submission.rsl_plate_num
|
||||
new_dict['source_row'] = plate.row
|
||||
new_dict['source_column'] = plate.column
|
||||
old_plate = plate.submission.rsl_plate_num
|
||||
output_samples.append(new_dict)
|
||||
df = pd.DataFrame.from_records(output_samples)
|
||||
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 = []
|
||||
# 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(QPushButton)]
|
||||
reagents = obj.prsr.parse_reagents(extraction_kit=extraction_kit)
|
||||
logger.debug(f"Got reagents: {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}")
|
||||
return obj, None
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user