Created omni-manager, omni-addit
This commit is contained in:
@@ -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):
|
||||
|
||||
|
||||
88
src/submissions/frontend/widgets/omni_add_edit.py
Normal file
88
src/submissions/frontend/widgets/omni_add_edit.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
227
src/submissions/frontend/widgets/omni_manager.py
Normal file
227
src/submissions/frontend/widgets/omni_manager.py
Normal 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)
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user