From 3d6a42b36fe94332d8d1668be908a7b9be44a52c Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 15 Nov 2024 09:34:28 -0600 Subject: [PATCH] Prior to creation of omni-search --- .../backend/db/models/submissions.py | 18 ++- src/submissions/backend/excel/reports.py | 2 +- src/submissions/backend/excel/writer.py | 3 +- src/submissions/frontend/widgets/app.py | 9 ++ .../frontend/widgets/omni_search.py | 128 ++++++++++++++++++ 5 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 src/submissions/frontend/widgets/omni_search.py diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index adde834..6eb740a 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -844,6 +844,9 @@ class BasicSubmission(BaseClass): ws.cell(row=item['row'], column=item['column'], value=item['value']) return input_excel + def custom_sample_writer(self, sample:dict) -> dict: + return sample + @classmethod def enforce_name(cls, instr: str, data: dict | None = {}) -> str: """ @@ -1692,6 +1695,8 @@ class WastewaterArtic(BasicSubmission): output['artic_technician'] = self.technician else: output['artic_technician'] = self.artic_technician + # logger.debug(full_data) + # logger.debug(output.keys()) output['gel_info'] = self.gel_info output['gel_image_path'] = self.gel_image output['dna_core_submission_number'] = self.dna_core_submission_number @@ -1850,13 +1855,17 @@ class WastewaterArtic(BasicSubmission): dict: Updated sample dictionary """ input_dict = super().parse_samples(input_dict) - # logger.debug(f"WWA input dict: {pformat(input_dict)}") + logger.debug(f"WWA input dict: {pformat(input_dict)}") input_dict['sample_type'] = "Wastewater Sample" # NOTE: Stop gap solution because WW is sloppy with their naming schemes try: input_dict['source_plate'] = input_dict['source_plate'].replace("WW20", "WW-20") except KeyError: pass + try: + input_dict['source_plate_number'] = int(input_dict['source_plate_number']) + except ValueError: + input_dict['source_plate_number'] = 0 # NOTE: Because generate_sample_object needs the submitter_id and the artic has the "({origin well})" # at the end, this has to be done here. No moving to sqlalchemy object :( input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip() @@ -2081,6 +2090,13 @@ class WastewaterArtic(BasicSubmission): logger.warning("No gel image found.") return input_excel + @classmethod + def custom_sample_writer(self, sample:dict) -> dict: + logger.debug("Wastewater Artic custom sample writer") + if sample['source_plate_number'] in [0, "0"]: + sample['source_plate_number'] = "control" + return sample + @classmethod def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]: """ diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 756f924..c7505ca 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -48,7 +48,7 @@ class ReportMaker(object): # logger.debug(f"Output daftaframe for xlsx: {df2.columns}") df = df.drop('id', axis=1) df = df.sort_values(['submitting_lab', "submitted_date"]) - logger.debug(f"Details dataframe:\n{df2}") + # logger.debug(f"Details dataframe:\n{df2}") return df, df2 def make_report_html(self, df: DataFrame) -> str: diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 71e8057..120d00c 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -291,7 +291,8 @@ class SampleWriter(object): """ multiples = ['row', 'column', 'assoc_id', 'submission_rank'] for sample in sample_list: - # logger.debug(f"Writing sample: {sample}") + sample = self.submission_type.get_submission_class().custom_sample_writer(sample) + logger.debug(f"Writing sample: {sample}") for assoc in zip(sample['row'], sample['column'], sample['submission_rank']): new = dict(row=assoc[0], column=assoc[1], submission_rank=assoc[2]) for k, v in sample.items(): diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index a847314..adcbeda 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -71,6 +71,7 @@ class App(QMainWindow): # logger.debug(f"Creating menu bar...") menuBar = self.menuBar() fileMenu = menuBar.addMenu("&File") + editMenu = menuBar.addMenu("&Edit") # NOTE: Creating menus using a title methodsMenu = menuBar.addMenu("&Methods") maintenanceMenu = menuBar.addMenu("&Monthly") @@ -85,6 +86,7 @@ class App(QMainWindow): methodsMenu.addAction(self.searchSample) maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinPCRAction) + editMenu.addAction(self.editReagentAction) def _createToolBar(self): """ @@ -115,6 +117,7 @@ class App(QMainWindow): self.githubAction = QAction("Github", self) self.yamlExportAction = QAction("Export Type Example", self) self.yamlImportAction = QAction("Import Type Template", self) + self.editReagentAction = QAction("Edit Reagent", self) def _connectActions(self): """ @@ -133,6 +136,7 @@ class App(QMainWindow): self.yamlExportAction.triggered.connect(self.export_ST_yaml) self.yamlImportAction.triggered.connect(self.import_ST_yaml) self.table_widget.pager.current_page.textChanged.connect(self.update_data) + self.editReagentAction.triggered.connect(self.edit_reagent) def showAbout(self): """ @@ -220,6 +224,11 @@ class App(QMainWindow): fname = select_save_file(obj=self, default_name="Submission Type Template.yml", extension="yml") shutil.copyfile(yaml_path, fname) + @check_authorization + def edit_reagent(self, *args, **kwargs): + dlg = EditReagent() + dlg.exec() + @check_authorization def import_ST_yaml(self, *args, **kwargs): fname = select_open_file(obj=self, file_extension="yml") diff --git a/src/submissions/frontend/widgets/omni_search.py b/src/submissions/frontend/widgets/omni_search.py new file mode 100644 index 0000000..d939204 --- /dev/null +++ b/src/submissions/frontend/widgets/omni_search.py @@ -0,0 +1,128 @@ +''' +Search box that performs fuzzy search for samples +''' +from pprint import pformat +from typing import Tuple +from pandas import DataFrame +from PyQt6.QtCore import QSortFilterProxyModel +from PyQt6.QtWidgets import ( + QLabel, QVBoxLayout, QDialog, + QComboBox, QTableView, QWidget, QLineEdit, QGridLayout +) +from backend.db.models import BasicSample +from .submission_table import pandasModel +import logging + +logger = logging.getLogger(f"submissions.{__name__}") + + +class SearchBox(QDialog): + + def __init__(self, parent): + super().__init__(parent) + self.layout = QGridLayout(self) + self.sample_type = QComboBox(self) + self.sample_type.setObjectName("sample_type") + self.sample_type.currentTextChanged.connect(self.update_widgets) + options = ["Any"] + [cls.__mapper_args__['polymorphic_identity'] for cls in BasicSample.__subclasses__()] + self.sample_type.addItems(options) + self.sample_type.setEditable(False) + self.setMinimumSize(600, 600) + self.sample_type.setMinimumWidth(self.minimumWidth()) + self.layout.addWidget(self.sample_type, 0, 0) + self.results = SearchResults() + self.layout.addWidget(self.results, 5, 0) + self.setLayout(self.layout) + self.update_widgets() + self.update_data() + + def update_widgets(self): + """ + Changes form inputs based on sample type + """ + deletes = [item for item in self.findChildren(FieldSearch)] + # logger.debug(deletes) + for item in deletes: + item.setParent(None) + if self.sample_type.currentText() == "Any": + self.type = BasicSample + else: + self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText()) + # logger.debug(f"Sample type: {self.type}") + searchables = self.type.get_searchables() + start_row = 1 + for iii, item in enumerate(searchables): + widget = FieldSearch(parent=self, label=item['label'], field_name=item['field']) + self.layout.addWidget(widget, start_row + iii, 0) + widget.search_widget.textChanged.connect(self.update_data) + self.update_data() + + def parse_form(self) -> dict: + """ + Converts form into dictionary. + + Returns: + dict: Fields dictionary + """ + fields = [item.parse_form() for item in self.findChildren(FieldSearch)] + return {item[0]: item[1] for item in fields if item[1] is not None} + + def update_data(self): + """ + Shows dataframe of relevant samples. + """ + # logger.debug(f"Running update_data with sample type: {self.type}") + fields = self.parse_form() + # logger.debug(f"Got fields: {fields}") + sample_list_creator = self.type.fuzzy_search(**fields) + data = self.type.samples_to_df(sample_list=sample_list_creator) + # logger.debug(f"Data: {data}") + self.results.setData(df=data) + + +class FieldSearch(QWidget): + + def __init__(self, parent, label, field_name): + super().__init__(parent) + self.layout = QVBoxLayout(self) + label_widget = QLabel(label) + self.layout.addWidget(label_widget) + self.search_widget = QLineEdit() + self.search_widget.setObjectName(field_name) + self.layout.addWidget(self.search_widget) + self.setLayout(self.layout) + self.search_widget.returnPressed.connect(self.enter_pressed) + + def enter_pressed(self): + """ + Triggered when enter is pressed on this input field. + """ + self.parent().update_data() + + def parse_form(self) -> Tuple: + field_value = self.search_widget.text() + if field_value == "": + field_value = None + return self.search_widget.objectName(), field_value + + +class SearchResults(QTableView): + + def __init__(self): + super().__init__() + self.doubleClicked.connect( + lambda x: BasicSample.query(submitter_id=x.sibling(x.row(), 0).data()).show_details(self)) + + def setData(self, df: DataFrame) -> None: + """ + sets data in model + """ + self.data = df + try: + self.data['id'] = self.data['id'].apply(str) + self.data['id'] = self.data['id'].str.zfill(3) + except (TypeError, KeyError): + logger.error("Couldn't format id string.") + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(pandasModel(self.data)) + self.setModel(proxy_model)