Created omni-manager, omni-addit

This commit is contained in:
lwark
2025-01-03 12:37:03 -06:00
parent 482b641569
commit b55258f677
19 changed files with 502 additions and 77 deletions

View File

@@ -12,8 +12,8 @@ from PyQt6.QtGui import QAction
from pathlib import Path
from markdown import markdown
from __init__ import project_path
from backend import SubmissionType, Reagent, BasicSample
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
from backend import SubmissionType, Reagent, BasicSample, Organization
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user
from .functions import select_save_file, select_open_file
# from datetime import date
from .pop_ups import HTMLPop, AlertPop
@@ -25,6 +25,7 @@ from .controls_chart import ControlsViewer
from .summary import Summary
from .turnaround import TurnaroundTime
from .omni_search import SearchBox
from .omni_manager import ManagerWindow
logger = logging.getLogger(f'submissions.{__name__}')
@@ -69,7 +70,7 @@ class App(QMainWindow):
fileMenu = menuBar.addMenu("&File")
editMenu = menuBar.addMenu("&Edit")
# NOTE: Creating menus using a title
methodsMenu = menuBar.addMenu("&Methods")
methodsMenu = menuBar.addMenu("&Search")
maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help")
helpMenu.addAction(self.helpAction)
@@ -82,6 +83,9 @@ class App(QMainWindow):
maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction)
editMenu.addAction(self.editReagentAction)
editMenu.addAction(self.manageOrgsAction)
if not is_power_user():
editMenu.setEnabled(False)
def _createToolBar(self):
"""
@@ -106,6 +110,7 @@ class App(QMainWindow):
self.yamlExportAction = QAction("Export Type Example", self)
self.yamlImportAction = QAction("Import Type Template", self)
self.editReagentAction = QAction("Edit Reagent", self)
self.manageOrgsAction = QAction("Manage Clients", self)
def _connectActions(self):
"""
@@ -123,6 +128,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.manageOrgsAction.triggered.connect(self.manage_orgs)
def showAbout(self):
"""
@@ -207,6 +213,11 @@ 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 manage_orgs(self):
dlg = ManagerWindow(parent=self, object_type=Organization, extras=[])
if dlg.exec():
new_org = dlg.parse_form()
logger.debug(new_org.__dict__)
class AddSubForm(QWidget):

View File

@@ -0,0 +1,88 @@
from typing import Any
from PyQt6.QtWidgets import (
QLabel, QDialog, QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit
)
from sqlalchemy import String, TIMESTAMP
from sqlalchemy.orm import InstrumentedAttribute
import logging
logger = logging.getLogger(f"submissions.{__name__}")
class AddEdit(QDialog):
def __init__(self, parent, instance: Any):
super().__init__(parent)
self.instance = instance
self.object_type = instance.__class__
self.layout = QGridLayout(self)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
fields = {k: v for k, v in self.object_type.__dict__.items() if
isinstance(v, InstrumentedAttribute) and k != "id"}
for key, field in fields.items():
try:
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
value=getattr(self.instance, key))
except AttributeError:
continue
self.layout.addWidget(widget, self.layout.rowCount(), 0)
self.layout.addWidget(self.buttonBox)
self.setWindowTitle(f"Add/Edit {self.object_type.__name__}")
self.setMinimumSize(600, 50 * len(fields))
self.setLayout(self.layout)
def parse_form(self):
results = {result[0]:result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)]}
# logger.debug(results)
model = self.object_type.get_pydantic_model()
model = model(**results)
try:
extras = list(model.model_extra.keys())
except AttributeError:
extras = []
fields = list(model.model_fields.keys()) + extras
for field in fields:
# logger.debug(result)
self.instance.__setattr__(field, model.__getattribute__(field))
return self.instance
class EditProperty(QWidget):
def __init__(self, parent: AddEdit, key: str, column_type: Any, value):
super().__init__(parent)
self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 1)
self.setObjectName(key)
match column_type:
case String():
self.widget = QLineEdit(self)
self.widget.setText(value)
case TIMESTAMP():
self.widget = QDateEdit(self)
self.widget.setDate(value)
case _:
logger.error(f"{column_type} not a supported type.")
self.widget = None
self.layout.addWidget(self.widget, 0, 1, 1, 3)
self.setLayout(self.layout)
def parse_form(self):
match self.widget:
case QLineEdit():
value = self.widget.text()
case QDateEdit():
value = self.widget.date()
case _:
value = None
return self.objectName(), value

View File

@@ -0,0 +1,227 @@
from typing import Any, List
from PyQt6.QtCore import QSortFilterProxyModel, Qt
from PyQt6.QtWidgets import (
QLabel, QDialog,
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit
)
from sqlalchemy import String, TIMESTAMP
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.orm.properties import ColumnProperty
from sqlalchemy.orm.relationships import _RelationshipDeclared
from pandas import DataFrame
from backend import db
import logging
from .omni_add_edit import AddEdit
from .omni_search import SearchBox
from frontend.widgets.submission_table import pandasModel
logger = logging.getLogger(f"submissions.{__name__}")
class ManagerWindow(QDialog):
"""
Initially this is a window to manage Organization Contacts, but hope to abstract it more later.
"""
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
super().__init__(parent)
self.object_type = self.original_type = object_type
self.instance = None
self.extras = extras
self.context = kwargs
self.layout = QGridLayout(self)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.setMinimumSize(600, 600)
sub_classes = ["Any"] + [cls.__name__ for cls in self.object_type.__subclasses__()]
if len(sub_classes) > 1:
self.sub_class = QComboBox(self)
self.sub_class.setObjectName("sub_class")
self.sub_class.addItems(sub_classes)
self.sub_class.currentTextChanged.connect(self.update_options)
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.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0)
self.options = QComboBox(self)
self.options.setObjectName("options")
self.update_options()
self.setLayout(self.layout)
self.setWindowTitle(f"Manage {self.object_type.__name__}")
def update_options(self):
"""
Changes form inputs based on sample type
"""
if self.sub_class:
self.object_type = getattr(db, self.sub_class.currentText())
options = [item.name for item in self.object_type.query()]
self.options.clear()
self.options.addItems(options)
self.options.setEditable(False)
self.options.setMinimumWidth(self.minimumWidth())
self.layout.addWidget(self.options, 1, 0, 1, 1)
self.add_button = QPushButton("Add New")
self.layout.addWidget(self.add_button, 1, 1, 1, 1)
self.options.currentTextChanged.connect(self.update_data)
self.add_button.clicked.connect(self.add_new)
self.update_data()
def update_data(self):
deletes = [item for item in self.findChildren(EditProperty)] + \
[item for item in self.findChildren(EditRelationship)] + \
[item for item in self.findChildren(QDialogButtonBox)]
for item in deletes:
item.setParent(None)
self.instance = self.object_type.query(name=self.options.currentText())
fields = {k: v for k, v in self.object_type.__dict__.items() if
isinstance(v, InstrumentedAttribute) and k != "id"}
for key, field in fields.items():
# logger.debug(f"Key: {key}, Value: {field}")
match field.property:
case ColumnProperty():
widget = EditProperty(self, key=key, column_type=field.property.expression.type,
value=getattr(self.instance, key))
case _RelationshipDeclared():
if key != "submissions":
widget = EditRelationship(self, key=key, entity=field.comparator.entity.class_,
value=getattr(self.instance, key))
else:
continue
case _:
continue
if widget:
self.layout.addWidget(widget, self.layout.rowCount(), 0, 1, 2)
self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2)
def parse_form(self):
results = [item.parse_form() for item in self.findChildren(EditProperty)]
# logger.debug(results)
for result in results:
# logger.debug(result)
self.instance.__setattr__(result[0], result[1])
return self.instance
def add_new(self):
dlg = AddEdit(parent=self, instance=self.object_type())
if dlg.exec():
new_instance = dlg.parse_form()
# logger.debug(new_instance.__dict__)
new_instance.save()
self.update_options()
class EditProperty(QWidget):
def __init__(self, parent: ManagerWindow, key: str, column_type: Any, value):
super().__init__(parent)
self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 1)
match column_type:
case String():
self.widget = QLineEdit(self)
self.widget.setText(value)
case TIMESTAMP():
self.widget = QDateEdit(self)
self.widget.setDate(value)
case _:
self.widget = None
self.layout.addWidget(self.widget, 0, 1, 1, 3)
self.setLayout(self.layout)
def parse_form(self):
match self.widget:
case QLineEdit():
value = self.widget.text()
case QDateEdit():
value = self.widget.date()
case _:
value = None
return self.objectName(), value
class EditRelationship(QWidget):
def __init__(self, parent, key: str, entity: Any, value):
super().__init__(parent)
self.entity = entity
self.data = value
self.label = QLabel(key.title().replace("_", " "))
self.setObjectName(key)
self.table = QTableView()
self.add_button = QPushButton("Add New")
self.add_button.clicked.connect(self.add_new)
self.existing_button = QPushButton("Add Existing")
self.existing_button.clicked.connect(self.add_existing)
self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 5)
self.layout.addWidget(self.table, 1, 0, 1, 8)
self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout)
self.set_data()
def parse_row(self, x):
context = {item: x.sibling(x.row(), self.data.columns.get_loc(item)).data() for item in self.data.columns}
try:
object = self.entity.query(**context)
except KeyError:
object = None
# logger.debug(object)
self.table.doubleClicked.disconnect()
self.add_edit(instance=object)
def add_new(self, instance: Any = None):
if not instance:
instance = self.entity()
dlg = AddEdit(self, instance=instance)
if dlg.exec():
new_instance = dlg.parse_form()
# logger.debug(new_instance.__dict__)
addition = getattr(self.parent().instance, self.objectName())
if isinstance(addition, InstrumentedList):
addition.append(new_instance)
self.parent().instance.save()
self.parent().update_data()
def add_existing(self):
dlg = SearchBox(self, object_type=self.entity, returnable=True, extras=[])
if dlg.exec():
rows = dlg.return_selected_rows()
# print(f"Rows selected: {[row for row in rows]}")
for row in rows:
instance = self.entity.query(**row)
# logger.debug(instance)
addition = getattr(self.parent().instance, self.objectName())
if isinstance(addition, InstrumentedList):
addition.append(instance)
self.parent().instance.save()
self.parent().update_data()
def set_data(self) -> None:
"""
sets data in model
"""
# logger.debug(self.data)
self.data = DataFrame.from_records([item.to_dict() for item in self.data])
try:
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
except (KeyError, AttributeError):
self.columns_of_interest = []
# try:
# self.data['id'] = self.data['id'].apply(str)
# self.data['id'] = self.data['id'].str.zfill(3)
# except (TypeError, KeyError) as e:
# logger.error(f"Couldn't format id string: {e}")
proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(pandasModel(self.data))
self.table.setModel(proxy_model)
self.table.doubleClicked.connect(self.parse_row)

View File

@@ -7,7 +7,7 @@ from pandas import DataFrame
from PyQt6.QtCore import QSortFilterProxyModel
from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog,
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox
)
from .submission_table import pandasModel
import logging
@@ -20,7 +20,7 @@ class SearchBox(QDialog):
The full search widget.
"""
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
def __init__(self, parent, object_type: Any, extras: List[str], returnable: bool = False, **kwargs):
super().__init__(parent)
self.object_type = self.original_type = object_type
self.extras = extras
@@ -44,6 +44,14 @@ class SearchBox(QDialog):
self.setWindowTitle(f"Search {self.object_type.__name__}")
self.update_widgets()
self.update_data()
if returnable:
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2)
self.results.doubleClicked.disconnect()
self.results.doubleClicked.connect(self.accept)
def update_widgets(self):
"""
@@ -63,7 +71,7 @@ class SearchBox(QDialog):
for iii, searchable in enumerate(self.object_type.searchables):
widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
widget.setObjectName(searchable)
self.layout.addWidget(widget, 1+iii, 0)
self.layout.addWidget(widget, 1 + iii, 0)
widget.search_widget.textChanged.connect(self.update_data)
self.update_data()
@@ -87,6 +95,13 @@ class SearchBox(QDialog):
# NOTE: Setting results moved to here from __init__ 202411118
self.results.setData(df=data)
def return_selected_rows(self):
rows = sorted(set(index.row() for index in
self.results.selectedIndexes()))
for index in rows:
output = {column:self.results.model().data(self.results.model().index(index, ii)) for ii, column in enumerate(self.results.data.columns)}
yield output
class FieldSearch(QWidget):
"""