Prior to creation of omni-search
This commit is contained in:
@@ -844,6 +844,9 @@ class BasicSubmission(BaseClass):
|
|||||||
ws.cell(row=item['row'], column=item['column'], value=item['value'])
|
ws.cell(row=item['row'], column=item['column'], value=item['value'])
|
||||||
return input_excel
|
return input_excel
|
||||||
|
|
||||||
|
def custom_sample_writer(self, sample:dict) -> dict:
|
||||||
|
return sample
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def enforce_name(cls, instr: str, data: dict | None = {}) -> str:
|
def enforce_name(cls, instr: str, data: dict | None = {}) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1692,6 +1695,8 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
output['artic_technician'] = self.technician
|
output['artic_technician'] = self.technician
|
||||||
else:
|
else:
|
||||||
output['artic_technician'] = self.artic_technician
|
output['artic_technician'] = self.artic_technician
|
||||||
|
# logger.debug(full_data)
|
||||||
|
# logger.debug(output.keys())
|
||||||
output['gel_info'] = self.gel_info
|
output['gel_info'] = self.gel_info
|
||||||
output['gel_image_path'] = self.gel_image
|
output['gel_image_path'] = self.gel_image
|
||||||
output['dna_core_submission_number'] = self.dna_core_submission_number
|
output['dna_core_submission_number'] = self.dna_core_submission_number
|
||||||
@@ -1850,13 +1855,17 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
dict: Updated sample dictionary
|
dict: Updated sample dictionary
|
||||||
"""
|
"""
|
||||||
input_dict = super().parse_samples(input_dict)
|
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"
|
input_dict['sample_type'] = "Wastewater Sample"
|
||||||
# NOTE: Stop gap solution because WW is sloppy with their naming schemes
|
# NOTE: Stop gap solution because WW is sloppy with their naming schemes
|
||||||
try:
|
try:
|
||||||
input_dict['source_plate'] = input_dict['source_plate'].replace("WW20", "WW-20")
|
input_dict['source_plate'] = input_dict['source_plate'].replace("WW20", "WW-20")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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})"
|
# 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 :(
|
# 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()
|
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.")
|
logger.warning("No gel image found.")
|
||||||
return input_excel
|
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
|
@classmethod
|
||||||
def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]:
|
def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ReportMaker(object):
|
|||||||
# logger.debug(f"Output daftaframe for xlsx: {df2.columns}")
|
# logger.debug(f"Output daftaframe for xlsx: {df2.columns}")
|
||||||
df = df.drop('id', axis=1)
|
df = df.drop('id', axis=1)
|
||||||
df = df.sort_values(['submitting_lab', "submitted_date"])
|
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
|
return df, df2
|
||||||
|
|
||||||
def make_report_html(self, df: DataFrame) -> str:
|
def make_report_html(self, df: DataFrame) -> str:
|
||||||
|
|||||||
@@ -291,7 +291,8 @@ class SampleWriter(object):
|
|||||||
"""
|
"""
|
||||||
multiples = ['row', 'column', 'assoc_id', 'submission_rank']
|
multiples = ['row', 'column', 'assoc_id', 'submission_rank']
|
||||||
for sample in sample_list:
|
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']):
|
for assoc in zip(sample['row'], sample['column'], sample['submission_rank']):
|
||||||
new = dict(row=assoc[0], column=assoc[1], submission_rank=assoc[2])
|
new = dict(row=assoc[0], column=assoc[1], submission_rank=assoc[2])
|
||||||
for k, v in sample.items():
|
for k, v in sample.items():
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class App(QMainWindow):
|
|||||||
# logger.debug(f"Creating menu bar...")
|
# logger.debug(f"Creating menu bar...")
|
||||||
menuBar = self.menuBar()
|
menuBar = self.menuBar()
|
||||||
fileMenu = menuBar.addMenu("&File")
|
fileMenu = menuBar.addMenu("&File")
|
||||||
|
editMenu = menuBar.addMenu("&Edit")
|
||||||
# NOTE: Creating menus using a title
|
# NOTE: Creating menus using a title
|
||||||
methodsMenu = menuBar.addMenu("&Methods")
|
methodsMenu = menuBar.addMenu("&Methods")
|
||||||
maintenanceMenu = menuBar.addMenu("&Monthly")
|
maintenanceMenu = menuBar.addMenu("&Monthly")
|
||||||
@@ -85,6 +86,7 @@ class App(QMainWindow):
|
|||||||
methodsMenu.addAction(self.searchSample)
|
methodsMenu.addAction(self.searchSample)
|
||||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||||
maintenanceMenu.addAction(self.joinPCRAction)
|
maintenanceMenu.addAction(self.joinPCRAction)
|
||||||
|
editMenu.addAction(self.editReagentAction)
|
||||||
|
|
||||||
def _createToolBar(self):
|
def _createToolBar(self):
|
||||||
"""
|
"""
|
||||||
@@ -115,6 +117,7 @@ class App(QMainWindow):
|
|||||||
self.githubAction = QAction("Github", self)
|
self.githubAction = QAction("Github", self)
|
||||||
self.yamlExportAction = QAction("Export Type Example", self)
|
self.yamlExportAction = QAction("Export Type Example", self)
|
||||||
self.yamlImportAction = QAction("Import Type Template", self)
|
self.yamlImportAction = QAction("Import Type Template", self)
|
||||||
|
self.editReagentAction = QAction("Edit Reagent", self)
|
||||||
|
|
||||||
def _connectActions(self):
|
def _connectActions(self):
|
||||||
"""
|
"""
|
||||||
@@ -133,6 +136,7 @@ class App(QMainWindow):
|
|||||||
self.yamlExportAction.triggered.connect(self.export_ST_yaml)
|
self.yamlExportAction.triggered.connect(self.export_ST_yaml)
|
||||||
self.yamlImportAction.triggered.connect(self.import_ST_yaml)
|
self.yamlImportAction.triggered.connect(self.import_ST_yaml)
|
||||||
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
|
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
|
||||||
|
self.editReagentAction.triggered.connect(self.edit_reagent)
|
||||||
|
|
||||||
def showAbout(self):
|
def showAbout(self):
|
||||||
"""
|
"""
|
||||||
@@ -220,6 +224,11 @@ class App(QMainWindow):
|
|||||||
fname = select_save_file(obj=self, default_name="Submission Type Template.yml", extension="yml")
|
fname = select_save_file(obj=self, default_name="Submission Type Template.yml", extension="yml")
|
||||||
shutil.copyfile(yaml_path, fname)
|
shutil.copyfile(yaml_path, fname)
|
||||||
|
|
||||||
|
@check_authorization
|
||||||
|
def edit_reagent(self, *args, **kwargs):
|
||||||
|
dlg = EditReagent()
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
@check_authorization
|
@check_authorization
|
||||||
def import_ST_yaml(self, *args, **kwargs):
|
def import_ST_yaml(self, *args, **kwargs):
|
||||||
fname = select_open_file(obj=self, file_extension="yml")
|
fname = select_open_file(obj=self, file_extension="yml")
|
||||||
|
|||||||
128
src/submissions/frontend/widgets/omni_search.py
Normal file
128
src/submissions/frontend/widgets/omni_search.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user