Mid-progress adding controls to pydantic creation.

This commit is contained in:
lwark
2024-11-21 08:46:41 -06:00
parent 506aac80c1
commit 7d1e6dc606
16 changed files with 224 additions and 187 deletions

View File

@@ -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):

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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}")