Mid-progress adding controls to pydantic creation.
This commit is contained in:
@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from markdown import markdown
|
||||
from __init__ import project_path
|
||||
from backend import SubmissionType, Reagent
|
||||
from backend import SubmissionType, Reagent, BasicSample
|
||||
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
|
||||
from .functions import select_save_file, select_open_file
|
||||
from datetime import date
|
||||
@@ -23,7 +23,7 @@ import logging, webbrowser, sys, shutil
|
||||
from .submission_table import SubmissionsSheet
|
||||
from .submission_widget import SubmissionFormContainer
|
||||
from .controls_chart import ControlsViewer
|
||||
from .sample_search import SampleSearchBox
|
||||
# from .sample_search import SampleSearchBox
|
||||
from .summary import Summary
|
||||
from .omni_search import SearchBox
|
||||
|
||||
@@ -138,6 +138,7 @@ class App(QMainWindow):
|
||||
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)
|
||||
self.destroyed.connect(self.final_commit)
|
||||
|
||||
def showAbout(self):
|
||||
"""
|
||||
@@ -186,7 +187,8 @@ class App(QMainWindow):
|
||||
"""
|
||||
Create a search for samples.
|
||||
"""
|
||||
dlg = SampleSearchBox(self)
|
||||
# dlg = SampleSearchBox(self)
|
||||
dlg = SearchBox(self, object_type=BasicSample, extras=[])
|
||||
dlg.exec()
|
||||
|
||||
def backup_database(self):
|
||||
@@ -251,6 +253,9 @@ class App(QMainWindow):
|
||||
def update_data(self):
|
||||
self.table_widget.sub_wid.setData(page=self.table_widget.pager.page_anchor, page_size=page_size)
|
||||
|
||||
def final_commit(self):
|
||||
logger.debug("Running final commit")
|
||||
self.ctx.database_session.commit()
|
||||
|
||||
class AddSubForm(QWidget):
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class AddReagentForm(QDialog):
|
||||
"""
|
||||
|
||||
def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
|
||||
reagent_name: str | None = None) -> None:
|
||||
reagent_name: str | None = None, kit: str | KitType | None = None) -> None:
|
||||
super().__init__()
|
||||
if reagent_name is None:
|
||||
reagent_name = reagent_role
|
||||
@@ -58,8 +58,16 @@ class AddReagentForm(QDialog):
|
||||
self.exp_input.setDate(QDate(1970, 1, 1))
|
||||
# NOTE: widget to get reagent type info
|
||||
self.type_input = QComboBox()
|
||||
self.type_input.setObjectName('type')
|
||||
self.type_input.addItems([item.name for item in ReagentRole.query()])
|
||||
self.type_input.setObjectName('role')
|
||||
if kit:
|
||||
match kit:
|
||||
case str():
|
||||
kit = KitType.query(name=kit)
|
||||
case _:
|
||||
pass
|
||||
self.type_input.addItems([item.name for item in ReagentRole.query() if kit in item.kit_types])
|
||||
else:
|
||||
self.type_input.addItems([item.name for item in ReagentRole.query()])
|
||||
# logger.debug(f"Trying to find index of {reagent_type}")
|
||||
# NOTE: convert input to user-friendly string?
|
||||
try:
|
||||
|
||||
@@ -19,17 +19,24 @@ class SearchBox(QDialog):
|
||||
|
||||
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
|
||||
super().__init__(parent)
|
||||
self.object_type = object_type
|
||||
# options = ["Any"] + [cls.__name__ for cls in self.object_type.__subclasses__()]
|
||||
# self.sub_class = QComboBox(self)
|
||||
# self.sub_class.setObjectName("sub_class")
|
||||
# self.sub_class.currentTextChanged.connect(self.update_widgets)
|
||||
# self.sub_class.addItems(options)
|
||||
# self.sub_class.setEditable(False)
|
||||
self.object_type = self.original_type = object_type
|
||||
self.extras = extras
|
||||
self.context = kwargs
|
||||
self.layout = QGridLayout(self)
|
||||
self.setMinimumSize(600, 600)
|
||||
# self.sub_class.setMinimumWidth(self.minimumWidth())
|
||||
# self.layout.addWidget(self.sub_class, 0, 0)
|
||||
self.results = SearchResults(parent=self, object_type=self.object_type, extras=extras, **kwargs)
|
||||
options = ["Any"] + [cls.__name__ for cls in self.object_type.__subclasses__()]
|
||||
if len(options) > 1:
|
||||
self.sub_class = QComboBox(self)
|
||||
self.sub_class.setObjectName("sub_class")
|
||||
self.sub_class.addItems(options)
|
||||
self.sub_class.currentTextChanged.connect(self.update_widgets)
|
||||
self.sub_class.setEditable(False)
|
||||
self.sub_class.setMinimumWidth(self.minimumWidth())
|
||||
self.layout.addWidget(self.sub_class, 0, 0)
|
||||
else:
|
||||
self.sub_class = None
|
||||
self.results = SearchResults(parent=self, object_type=self.object_type, extras=self.extras, **kwargs)
|
||||
logger.debug(f"results: {self.results}")
|
||||
self.layout.addWidget(self.results, 5, 0)
|
||||
self.setLayout(self.layout)
|
||||
self.setWindowTitle(f"Search {self.object_type.__name__}")
|
||||
@@ -40,10 +47,23 @@ class SearchBox(QDialog):
|
||||
"""
|
||||
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 not self.sub_class:
|
||||
self.update_data()
|
||||
else:
|
||||
if self.sub_class.currentText() == "Any":
|
||||
self.object_type = self.original_type
|
||||
else:
|
||||
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
|
||||
logger.debug(f"{self.object_type} searchables: {self.object_type.searchables}")
|
||||
for iii, searchable in enumerate(self.object_type.searchables):
|
||||
self.widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
|
||||
self.layout.addWidget(self.widget, 1, 0)
|
||||
self.widget.search_widget.textChanged.connect(self.update_data)
|
||||
widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
|
||||
widget.setObjectName(searchable)
|
||||
self.layout.addWidget(widget, 1+iii, 0)
|
||||
widget.search_widget.textChanged.connect(self.update_data)
|
||||
self.update_data()
|
||||
|
||||
def parse_form(self) -> dict:
|
||||
@@ -60,11 +80,11 @@ class SearchBox(QDialog):
|
||||
"""
|
||||
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.object_type.fuzzy_search(**fields)
|
||||
data = self.object_type.results_to_df(objects=sample_list_creator)
|
||||
# Setting results moved to here from __init__ 202411118
|
||||
self.results.setData(df=data)
|
||||
|
||||
|
||||
@@ -108,7 +128,6 @@ class SearchResults(QTableView):
|
||||
sets data in model
|
||||
"""
|
||||
self.data = df
|
||||
print(self.data)
|
||||
try:
|
||||
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
|
||||
except KeyError:
|
||||
@@ -125,14 +144,15 @@ class SearchResults(QTableView):
|
||||
|
||||
def parse_row(self, x):
|
||||
context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest}
|
||||
logger.debug(f"Context: {context}")
|
||||
try:
|
||||
object = self.object_type.query(**{self.object_type.search: context[self.object_type.search]})
|
||||
# object = self.object_type.query(**{self.object_type.searchables: context[self.object_type.searchables]})
|
||||
object = self.object_type.query(**context)
|
||||
except KeyError:
|
||||
object = None
|
||||
try:
|
||||
object.edit_from_search(**context)
|
||||
except AttributeError:
|
||||
pass
|
||||
object.edit_from_search(obj=self.parent, **context)
|
||||
except AttributeError as e:
|
||||
logger.error(f"Error getting object function: {e}")
|
||||
self.doubleClicked.disconnect()
|
||||
self.parent.update_data()
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
'''
|
||||
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 SampleSearchBox(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)
|
||||
|
||||
@@ -45,7 +45,8 @@ class SubmissionDetails(QDialog):
|
||||
self.btn.clicked.connect(self.export)
|
||||
self.back = QPushButton("Back")
|
||||
self.back.setFixedWidth(100)
|
||||
self.back.clicked.connect(self.back_function)
|
||||
# self.back.clicked.connect(self.back_function)
|
||||
self.back.clicked.connect(self.webview.back)
|
||||
self.layout.addWidget(self.back, 0, 0, 1, 1)
|
||||
self.layout.addWidget(self.btn, 0, 1, 1, 9)
|
||||
self.layout.addWidget(self.webview, 1, 0, 10, 10)
|
||||
@@ -63,8 +64,8 @@ class SubmissionDetails(QDialog):
|
||||
self.reagent_details(reagent=sub)
|
||||
self.webview.page().setWebChannel(self.channel)
|
||||
|
||||
def back_function(self):
|
||||
self.webview.back()
|
||||
# def back_function(self):
|
||||
# self.webview.back()
|
||||
|
||||
def activate_export(self):
|
||||
title = self.webview.title()
|
||||
@@ -75,7 +76,11 @@ class SubmissionDetails(QDialog):
|
||||
# logger.debug(f"Updating export plate to: {self.export_plate}")
|
||||
else:
|
||||
self.btn.setEnabled(False)
|
||||
if title == self.webview.history().items()[0].title():
|
||||
try:
|
||||
check = self.webview.history().items()[0].title()
|
||||
except IndexError as e:
|
||||
check = title
|
||||
if title == check:
|
||||
# logger.debug("Disabling back button")
|
||||
self.back.setEnabled(False)
|
||||
else:
|
||||
@@ -96,7 +101,7 @@ class SubmissionDetails(QDialog):
|
||||
exclude = ['submissions', 'excluded', 'colour', 'tooltip']
|
||||
base_dict['excluded'] = exclude
|
||||
template = sample.get_details_template()
|
||||
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
|
||||
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
html = template.render(sample=base_dict, css=css)
|
||||
|
||||
@@ -31,6 +31,7 @@ class MyQComboBox(QComboBox):
|
||||
"""
|
||||
Custom combobox that disables wheel events until focussed on.
|
||||
"""
|
||||
|
||||
def __init__(self, scrollWidget=None, *args, **kwargs):
|
||||
super(MyQComboBox, self).__init__(*args, **kwargs)
|
||||
self.scrollWidget = scrollWidget
|
||||
@@ -48,6 +49,7 @@ class MyQDateEdit(QDateEdit):
|
||||
"""
|
||||
Custom date editor that disables wheel events until focussed on.
|
||||
"""
|
||||
|
||||
def __init__(self, scrollWidget=None, *args, **kwargs):
|
||||
super(MyQDateEdit, self).__init__(*args, **kwargs)
|
||||
self.scrollWidget = scrollWidget
|
||||
@@ -150,7 +152,7 @@ class SubmissionFormContainer(QWidget):
|
||||
|
||||
@report_result
|
||||
def add_reagent(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
|
||||
name: str | None = None) -> Tuple[PydReagent, Report]:
|
||||
name: str | None = None, kit: str | KitType | None = None) -> Tuple[PydReagent, Report]:
|
||||
"""
|
||||
Action to create new reagent in DB.
|
||||
|
||||
@@ -167,7 +169,8 @@ class SubmissionFormContainer(QWidget):
|
||||
if isinstance(reagent_lot, bool):
|
||||
reagent_lot = ""
|
||||
# NOTE: create form
|
||||
dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_role=reagent_role, expiry=expiry, reagent_name=name)
|
||||
dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_role=reagent_role, expiry=expiry, reagent_name=name,
|
||||
kit=kit)
|
||||
if dlg.exec():
|
||||
# NOTE: extract form info
|
||||
info = dlg.parse_form()
|
||||
@@ -228,7 +231,7 @@ class SubmissionFormWidget(QWidget):
|
||||
# self.scrape_reagents(self.pyd.extraction_kit)
|
||||
self.scrape_reagents(self.extraction_kit)
|
||||
|
||||
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType| None = None,
|
||||
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,
|
||||
disable: bool = False) -> "self.InfoItem":
|
||||
"""
|
||||
@@ -506,7 +509,8 @@ class SubmissionFormWidget(QWidget):
|
||||
return None, None
|
||||
return self.input.objectName(), dict(value=value, missing=self.missing)
|
||||
|
||||
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
|
||||
def set_widget(self, parent: QWidget, key: str, value: dict,
|
||||
submission_type: str | SubmissionType | None = None,
|
||||
sub_obj: BasicSubmission | None = None) -> QWidget:
|
||||
"""
|
||||
Creates form widget
|
||||
@@ -682,9 +686,11 @@ class SubmissionFormWidget(QWidget):
|
||||
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
|
||||
if dlg.exec():
|
||||
wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot,
|
||||
reagent_role=self.reagent.role,
|
||||
expiry=self.reagent.expiry,
|
||||
name=self.reagent.name)
|
||||
reagent_role=self.reagent.role,
|
||||
expiry=self.reagent.expiry,
|
||||
name=self.reagent.name,
|
||||
kit=self.extraction_kit
|
||||
)
|
||||
return wanted_reagent, report
|
||||
else:
|
||||
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||
@@ -772,4 +778,3 @@ class SubmissionFormWidget(QWidget):
|
||||
self.setObjectName(f"lot_{reagent.role}")
|
||||
self.addItems(relevant_reagents)
|
||||
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}")
|
||||
|
||||
Reference in New Issue
Block a user