diff --git a/TODO.md b/TODO.md index 997b1f7..477c102 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- [ ] move functions from widgets.functions to... app? - [x] Apply below fix to all other date-based queries. - [x] Fix Control graph chart bug that excludes today's controls. - [x] Convert logger to a custom class. diff --git a/src/submissions/__main__.py b/src/submissions/__main__.py index 7232a1b..f97a423 100644 --- a/src/submissions/__main__.py +++ b/src/submissions/__main__.py @@ -8,8 +8,6 @@ if check_if_app(): # NOTE: setup custom logger logging.setLoggerClass(CustomLogger) -# logger = logging.getLogger("submissions") -# logger = setup_logger(verbosity=3) from PyQt6.QtWidgets import QApplication from frontend.widgets.app import App diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 5ebf7d2..73fdacc 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1573,17 +1573,13 @@ class BacterialCulture(BasicSubmission): def get_provisional_controls(self, include: List[str] = []): # NOTE To ensure Samples are done last. include = sorted(include) - logger.debug(include) + # logger.debug(include) pos_str = "(ATCC)|(MCS)" pos_regex = re.compile(rf"^{pos_str}") neg_str = "(EN)" neg_regex = re.compile(rf"^{neg_str}") - total_str = pos_str + "|" + neg_str - total_regex = re.compile(rf"^{total_str}") output = [] for item in include: - # if self.controls: - # logger.debug(item) match item: case "Positive": if self.controls: diff --git a/src/submissions/frontend/visualizations/irida_charts.py b/src/submissions/frontend/visualizations/irida_charts.py index e66caf1..c27bc7b 100644 --- a/src/submissions/frontend/visualizations/irida_charts.py +++ b/src/submissions/frontend/visualizations/irida_charts.py @@ -21,7 +21,6 @@ class IridaFigure(CustomFigure): self.df = df self.construct_chart(df=df, modes=modes, start_date=settings['start_date'], end_date=settings['end_date']) - def construct_chart(self, df: pd.DataFrame, modes: list, start_date: date, end_date:date): """ Creates a plotly chart for controls from a pandas dataframe diff --git a/src/submissions/frontend/widgets/__init__.py b/src/submissions/frontend/widgets/__init__.py index ff8d8fb..887630a 100644 --- a/src/submissions/frontend/widgets/__init__.py +++ b/src/submissions/frontend/widgets/__init__.py @@ -17,4 +17,4 @@ from .submission_widget import * from .summary import * from .turnaround import * from .omni_add_edit import * -from .omni_manager import * +from .omni_manager_pydant import * diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 511859e..f3fef25 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -1,7 +1,7 @@ """ Constructs main application. """ -import getpass, logging, webbrowser, sys, shutil +import getpass, logging, webbrowser, sys from pprint import pformat from PyQt6.QtCore import qInstallMessageHandler from PyQt6.QtWidgets import ( @@ -13,8 +13,7 @@ from PyQt6.QtGui import QAction from pathlib import Path from markdown import markdown from pandas import ExcelWriter -from __init__ import project_path -from backend import SubmissionType, Reagent, BasicSample, Organization, KitType, BasicSubmission +from backend import Reagent, BasicSample, Organization, KitType, BasicSubmission from tools import ( check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, under_development @@ -30,7 +29,6 @@ from .summary import Summary from .turnaround import TurnaroundTime from .concentrations import Concentrations from .omni_search import SearchBox -from .omni_manager import ManagerWindow logger = logging.getLogger(f'submissions.{__name__}') @@ -83,7 +81,6 @@ class App(QMainWindow): helpMenu.addAction(self.githubAction) fileMenu.addAction(self.importAction) fileMenu.addAction(self.archiveSubmissionsAction) - # fileMenu.addAction(self.yamlImportAction) methodsMenu.addAction(self.searchSample) maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinPCRAction) @@ -123,7 +120,7 @@ class App(QMainWindow): """ connect menu and tool bar item to functions """ - self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission) + self.importAction.triggered.connect(lambda fname: self.table_widget.formwidget.import_submission_function(fname=fname)) self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent) self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions) self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr) @@ -192,7 +189,6 @@ class App(QMainWindow): # TODO: Change this to the Pydantic version. def manage_orgs(self): from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd - # dlg = ManagerWindow(parent=self, object_type=Organization, extras=[], add_edit='edit', managers=set()) dlg = ManagerWindowPyd(parent=self, object_type=Organization, extras=[], add_edit='edit', managers=set()) if dlg.exec(): new_org = dlg.parse_form() @@ -202,10 +198,10 @@ class App(QMainWindow): from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd dlg = ManagerWindowPyd(parent=self, object_type=KitType, extras=[], add_edit='edit', managers=set()) if dlg.exec(): - logger.debug("\n\nBeginning parsing\n\n") + # logger.debug("\n\nBeginning parsing\n\n") output = dlg.parse_form() - logger.debug(f"Kit output: {pformat(output.__dict__)}") - logger.debug("\n\nBeginning transformation\n\n") + # logger.debug(f"Kit output: {pformat(output.__dict__)}") + # logger.debug("\n\nBeginning transformation\n\n") sql = output.to_sql() assert isinstance(sql, KitType) sql.save() diff --git a/src/submissions/frontend/widgets/concentrations.py b/src/submissions/frontend/widgets/concentrations.py index c077402..bc11c33 100644 --- a/src/submissions/frontend/widgets/concentrations.py +++ b/src/submissions/frontend/widgets/concentrations.py @@ -1,7 +1,7 @@ """ Pane showing BC control concentrations summary. """ -from PyQt6.QtWidgets import QWidget, QPushButton, QCheckBox, QLabel +from PyQt6.QtWidgets import QWidget, QPushButton, QLabel from .info_tab import InfoPane from backend.excel.reports import ConcentrationMaker from frontend.visualizations.concentrations_chart import ConcentrationsChart @@ -25,6 +25,7 @@ class Concentrations(InfoPane): self.pos_neg = CheckableComboBox(parent=self) self.pos_neg.model().itemChanged.connect(self.update_data) self.pos_neg.setEditable(False) + self.pos_neg.addItem("Select", header=True) self.pos_neg.addItem("Positive") self.pos_neg.addItem("Negative") self.pos_neg.addItem("Samples", start_checked=False) @@ -46,7 +47,6 @@ class Concentrations(InfoPane): super().update_data() months = self.diff_month(self.start_date, self.end_date) # logger.debug(f"Box checked: {self.all_box.isChecked()}") - # chart_settings = dict(start_date=self.start_date, end_date=self.end_date, controls_only=self.all_box.isChecked()) chart_settings = dict(start_date=self.start_date, end_date=self.end_date, include=include) self.report_obj = ConcentrationMaker(**chart_settings) diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index c61de74..3c843a1 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -108,7 +108,7 @@ class ControlsViewer(InfoPane): parent=self, months=months ) - logger.debug(f"Chart settings: {chart_settings}") + # logger.debug(f"Chart settings: {chart_settings}") self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx) self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name) if issubclass(self.fig.__class__, CustomFigure): diff --git a/src/submissions/frontend/widgets/date_type_picker.py b/src/submissions/frontend/widgets/date_type_picker.py index 71c5ea6..ccad3ca 100644 --- a/src/submissions/frontend/widgets/date_type_picker.py +++ b/src/submissions/frontend/widgets/date_type_picker.py @@ -1,6 +1,5 @@ from PyQt6.QtWidgets import ( - QLabel, QVBoxLayout, QDialog, - QDialogButtonBox, QMessageBox, QComboBox + QVBoxLayout, QDialog, QDialogButtonBox ) from .misc import CheckableComboBox, StartEndDatePicker from backend.db import SubmissionType @@ -28,7 +27,8 @@ class DateTypePicker(QDialog): self.setLayout(self.layout) def parse_form(self): - sub_types = [self.typepicker.itemText(i) for i in range(self.typepicker.count()) if self.typepicker.itemChecked(i)] + # sub_types = [self.typepicker.itemText(i) for i in range(self.typepicker.count()) if self.typepicker.itemChecked(i)] + sub_types = self.typepicker.get_checked() start_date = self.datepicker.start_date.date().toPyDate() end_date = self.datepicker.end_date.date().toPyDate() return dict(submissiontype=sub_types, start_date=start_date, end_date=end_date) diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index e527918..b575d99 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -74,10 +74,10 @@ class EquipmentUsage(QDialog): self.layout.addWidget(self.check, 0, 0) self.check.stateChanged.connect(self.check_all) for iii, item in enumerate(["Role", "Equipment", "Process", "Tips"], start=1): - l = QLabel(item) - l.setMaximumWidth(200) - l.setMinimumWidth(200) - self.layout.addWidget(l, 0, iii, alignment=Qt.AlignmentFlag.AlignRight) + label = QLabel(item) + label.setMaximumWidth(200) + label.setMinimumWidth(200) + self.layout.addWidget(label, 0, iii, alignment=Qt.AlignmentFlag.AlignRight) self.setLayout(self.layout) def check_all(self): diff --git a/src/submissions/frontend/widgets/info_tab.py b/src/submissions/frontend/widgets/info_tab.py index 6b626dc..5e70aad 100644 --- a/src/submissions/frontend/widgets/info_tab.py +++ b/src/submissions/frontend/widgets/info_tab.py @@ -1,7 +1,7 @@ """ A pane to show info e.g. cost reports and turnaround times. """ -from datetime import date, datetime +from datetime import date from PyQt6.QtCore import QSignalBlocker from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import QWidget, QGridLayout @@ -32,11 +32,9 @@ class InfoPane(QWidget): @report_result def update_data(self, *args, **kwargs): report = Report() - # self.start_date = self.datepicker.start_date.date().toPyDate() - # self.end_date = self.datepicker.end_date.date().toPyDate() self.start_date = self.datepicker.start_date.date().toPyDate() self.end_date = self.datepicker.end_date.date().toPyDate() - logger.debug(f"Start date: {self.start_date}, End date: {self.end_date}") + # logger.debug(f"Start date: {self.start_date}, End date: {self.end_date}") if self.datepicker.start_date.date() > self.datepicker.end_date.date(): lastmonth = self.datepicker.end_date.date().addDays(-31) msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}." diff --git a/src/submissions/frontend/widgets/misc.py b/src/submissions/frontend/widgets/misc.py index 46f9036..1eb8729 100644 --- a/src/submissions/frontend/widgets/misc.py +++ b/src/submissions/frontend/widgets/misc.py @@ -44,7 +44,7 @@ class StartEndDatePicker(QWidget): class CheckableComboBox(QComboBox): # once there is a checkState set, it is rendered - # here we assume default Unchecked + # here we assume default checked def addItem(self, item, header: bool = False, start_checked: bool = True): super(CheckableComboBox, self).addItem(item) @@ -64,8 +64,7 @@ class CheckableComboBox(QComboBox): self.updated.emit() def get_checked(self): - checked = [self.itemText(i) for i in range(self.count()) if self.itemChecked(i)] - return checked + return [self.itemText(i) for i in range(self.count()) if self.itemChecked(i)] class Pagifier(QWidget): diff --git a/src/submissions/frontend/widgets/omni_add_edit.py b/src/submissions/frontend/widgets/omni_add_edit.py index 29f53cf..c02d23a 100644 --- a/src/submissions/frontend/widgets/omni_add_edit.py +++ b/src/submissions/frontend/widgets/omni_add_edit.py @@ -1,7 +1,6 @@ """ A widget to handle adding/updating any database object. """ -from copy import deepcopy from datetime import date from pprint import pformat from typing import Any, Tuple @@ -11,7 +10,7 @@ from PyQt6.QtWidgets import ( QCheckBox ) from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT, JSON, BLOB -from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty +from sqlalchemy.orm import ColumnProperty import logging from sqlalchemy.orm.relationships import _RelationshipDeclared from tools import Report, report_result @@ -23,18 +22,11 @@ class AddEdit(QDialog): def __init__(self, parent, instance: Any | None = None, managers: set = set()): super().__init__(parent) - logger.debug(f"Managers: {managers}") + # logger.debug(f"Managers: {managers}") self.instance = instance self.object_type = instance.__class__ - # self.managers = deepcopy(managers) self.managers = managers - # if instance.level < 2: - # try: - # logger.debug(f"Parent instance: {self.parent().instance}") - # self.managers.add(self.parent().instance) - # except AttributeError: - # pass - logger.debug(f"Managers: {managers}") + # logger.debug(f"Managers: {managers}") self.layout = QGridLayout(self) QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel self.buttonBox = QDialogButtonBox(QBtn) @@ -72,11 +64,11 @@ class AddEdit(QDialog): report = Report() parsed = {result[0].strip(":"): result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]} - logger.debug(f"Parsed form: {parsed}") + # logger.debug(f"Parsed form: {parsed}") model = self.object_type.pydantic_model - logger.debug(f"Model type: {model.__name__}") + # logger.debug(f"Model type: {model.__name__}") if model.__name__ == "PydElastic": - logger.debug(f"We have an elastic model.") + # logger.debug(f"We have an elastic model.") parsed['instance'] = self.instance # NOTE: Hand-off to pydantic model for validation. # NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts. @@ -120,7 +112,7 @@ class EditProperty(QWidget): # logger.debug(self.parent().managers) for manager in self.parent().managers: if self.name in manager.aliases: - logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}") + # logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}") choices = [manager.name] self.widget.setEnabled(False) break @@ -139,7 +131,7 @@ class EditProperty(QWidget): self.widget.addItems(choices) def column_property_set(self, column_property, value=None): - logger.debug(f"Column Property: {column_property['class_attr'].expression} {column_property}, Value: {value}") + # logger.debug(f"Column Property: {column_property['class_attr'].expression} {column_property}, Value: {value}") match column_property['class_attr'].expression.type: case String(): if value is None: @@ -184,7 +176,8 @@ class EditProperty(QWidget): check = self.widget except AttributeError: return None, None - match self.widget: + # match self.widget + match check: case QLineEdit(): value = self.widget.text() case QDateEdit(): @@ -198,5 +191,3 @@ class EditProperty(QWidget): case _: value = None return self.objectName(), value - - diff --git a/src/submissions/frontend/widgets/omni_manager.py b/src/submissions/frontend/widgets/omni_manager.py deleted file mode 100644 index 783bdda..0000000 --- a/src/submissions/frontend/widgets/omni_manager.py +++ /dev/null @@ -1,594 +0,0 @@ -""" -Provides a screen for managing all attributes of a database object. -""" -import json, logging -from pprint import pformat -from typing import Any, List, Literal -from PyQt6.QtCore import QSortFilterProxyModel, Qt -from PyQt6.QtGui import QAction, QCursor -from PyQt6.QtWidgets import ( - QLabel, QDialog, - QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit, QMenu, - QDoubleSpinBox, QSpinBox, QCheckBox, QTextEdit, QVBoxLayout, QHBoxLayout -) -from sqlalchemy import String, TIMESTAMP, FLOAT, INTEGER, JSON, BLOB -from sqlalchemy.orm import InstrumentedAttribute -from sqlalchemy.orm.properties import ColumnProperty -from sqlalchemy.orm.relationships import _RelationshipDeclared -from pandas import DataFrame -from backend import db -from tools import check_object_in_manager -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], - manager: Any | None = None, - add_edit: Literal['add', 'edit'] = 'edit', - **kwargs): - super().__init__(parent) - self.class_object = self.original_type = object_type - self.add_edit = add_edit - # NOTE: Should I pass in an instance? - self.instance = None - if manager is None: - try: - self.manager = self.parent().instance - except AttributeError: - self.manager = None - else: - self.manager = manager - # logger.debug(f"Managers: {managers}") - 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.class_object.__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 - if self.add_edit == "edit": - self.options = QComboBox(self) - self.options.setObjectName("options") - self.update_options() - else: - self.update_data(initial=True) - self.setLayout(self.layout) - self.setWindowTitle(f"Manage {self.class_object.__name__} - Manager: {self.manager}") - - def update_options(self) -> None: - """ - Changes form inputs based on sample type - """ - # logger.debug(f"Instance: {self.instance}") - if self.sub_class: - self.class_object = getattr(db, self.sub_class.currentText()) - # logger.debug(f"From update options, managers: {self.managers}") - try: - query_kwargs = {self.parent().instance.query_alias: self.parent().instance} - except AttributeError as e: - # logger.debug(f"Couldn't set query kwargs due to: {e}") - query_kwargs = {} - # logger.debug(f"Query kwargs: {query_kwargs}") - options = [item.name for item in self.class_object.query(**query_kwargs)] - # logger.debug(f"self.class_object: {self.class_object}") - if self.instance: - try: - inserter = options.pop(options.index(self.instance.name)) - except ValueError: - inserter = self.instance.name - options.insert(0, inserter) - 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.add_button.clicked.connect(self.add_new) - self.options.currentTextChanged.connect(self.update_data) - # logger.debug(f"Instance: {self.instance}") - self.update_data() - - def update_data(self, initial: bool = False) -> None: - """ - Performs updating of widgets on first run and after options change. - - Returns: - None - """ - # NOTE: Remove all old widgets. - 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) - # logger.debug(f"Current options text lower: {self.options.currentText().lower()}") - if self.add_edit == "edit" and initial: - # logger.debug(f"Querying with {self.options.currentText()}") - self.instance = self.class_object.query(name=self.options.currentText(), limit=1) - # logger.debug(f"Instance: {self.instance}") - if not self.instance: - self.instance = self.class_object() - # logger.debug(f"self.instance: {self.instance}") - fields = self.instance.omnigui_instance_dict - for key, field in fields.items(): - try: - value = getattr(self.instance, key) - except AttributeError: - value = None - match field['class_attr'].property: - # NOTE: ColumnProperties will be directly edited. - case ColumnProperty(): - # NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP - widget = EditProperty(self, key=key, column_type=field, - value=value) - # NOTE: RelationshipDeclareds will be given a list of existing related objects. - case _RelationshipDeclared(): - if key != "submissions": - # NOTE: field.comparator.class_object.class_ gives the relationship class - widget = EditRelationship(self, key=key, class_object=field['class_attr'].comparator.entity.class_, - value=value) - else: - continue - case _: - continue - if widget: - self.layout.addWidget(widget, self.layout.rowCount(), 0, 1, 2) - # NOTE: Add OK|Cancel to bottom of dialog. - self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2) - - def parse_form(self) -> Any: - """ - Returns the instance associated with this window. - - Returns: - Any: The instance with updated fields. - """ - results = [item.parse_form() for item in self.findChildren(EditProperty)] - for result in results: - # logger.debug(f"Incoming result: {result}") - setattr(self.instance, result['field'], result['value']) - # logger.debug(f"Set result: {getattr(self.instance, result['field'])}") - results = [item.parse_form() for item in self.findChildren(EditRelationship)] - for result in results: - logger.debug(f"Incoming result: {result}") - if not getattr(self.instance, result['field']): - setattr(self.instance, result['field'], result['value']) - logger.debug(f"Set result: {getattr(self.instance, result['field'])}") - logger.debug(f"Instance coming from parsed form: {self.instance.__dict__}") - return self.instance - - def add_new(self): - new_instance = self.class_object() - self.instance = new_instance - self.update_options() - - def add_to_json(self, caller_child=None): - try: - name = caller_child.objectName() - except AttributeError: - name = "No Caller" - jsonedit = JsonEditScreen(parent=self, key=name) - if jsonedit.exec(): - data = jsonedit.parse_form() - logger.debug(f"Data: {pformat(data)}") - current_value = getattr(self.instance, name) - if isinstance(jsonedit.json_field, dict): - value = data - elif isinstance(jsonedit.json_field, list): - if isinstance(data, list): - value = current_value + data - else: - value = current_value + [data] - setattr(self.instance, name, value) - - def toggle_textedit(self, caller_child=None): - already_exists = self.findChildren(LargeTextEdit) - if not already_exists: - try: - name = caller_child.objectName() - except AttributeError: - name = "No Caller" - logger.debug(f"Name: {name}, instance: {self.instance}") - textedit = LargeTextEdit(parent=self, key=name) - self.layout.addWidget(textedit, 1, self.layout.columnCount(), self.layout.rowCount() - 1, 1) - data = getattr(self.instance, name) - logger.debug(f"Data: {data}") - data = json.dumps(data, indent=4) - textedit.widget.setText(data) - else: - for item in already_exists: - item.setParent(None) - item.destroy() - - -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) - self.setObjectName(key) - logger.debug(f"Column type for {key}: {column_type['class_attr'].property.expression.type}") - match column_type['class_attr'].property.expression.type: - case String(): - self.widget = QLineEdit(self) - self.widget.setText(value) - case INTEGER(): - if isinstance(column_type['instance_attr'], bool): - self.widget = QCheckBox() - self.widget.setChecked(value) - else: - if value is None: - value = 0 - self.widget = QSpinBox() - self.widget.setMaximum(999) - self.widget.setValue(value) - case FLOAT(): - if not value: - value = 0.0 - self.widget = QDoubleSpinBox() - self.widget.setMaximum(999.99) - self.widget.setValue(value) - case TIMESTAMP(): - self.widget = QDateEdit(self) - self.widget.setDate(value) - case JSON(): - self.widget = JsonEditButton(parent=self, key=key) - self.widget.viewButton.clicked.connect(lambda: self.parent().toggle_textedit(self.widget)) - self.widget.addButton.clicked.connect(lambda: self.parent().add_to_json(self.widget)) - case BLOB(): - self.widget = QLabel("BLOB Under construction") - 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 QSpinBox() | QDoubleSpinBox(): - value = self.widget.value() - case QCheckBox(): - value = self.widget.isChecked() - case _: - value = None - return dict(field=self.objectName(), value=value) - - -class EditRelationship(QWidget): - - def __init__(self, parent, key: str, class_object: Any, value): - super().__init__(parent) - self.class_object = class_object #: The class of interest - self.setParent(parent) - # logger.debug(f"Edit relationship class_object: {self.class_object}") - self.label = QLabel(key.title().replace("_", " ")) - self.setObjectName(key) #: key is the name of the relationship this represents - self.relationship = getattr(self.parent().instance.__class__, - key) #: relationship object for type differentiation - # logger.debug(f"self.relationship: {self.relationship}") - # logger.debug(f"Relationship uses list: {self.relationship.property.uselist}") - # NOTE: value is a database object in this case. - # logger.debug(f"Data for edit relationship: {self.data}") - self.widget = 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.existing_button.setEnabled(self.class_object.level == 1) - if not isinstance(value, list): - if value is not None: - value = [value] - else: - value = [] - self.data = value - checked_manager, is_primary = check_object_in_manager(self.parent().manager, self.objectName()) - if checked_manager: - logger.debug(f"Checked manager for {self.objectName()}: {checked_manager}") - logger.debug(f"Omni will inherit: {self.class_object.omni_inheritable} from {self.parent().class_object}") - if checked_manager is not None and not self.data and self.objectName() in self.parent().class_object.omni_inheritable: - logger.debug(f"Setting {checked_manager} in self.data") - self.data = [checked_manager] - if not self.relationship.property.uselist: - self.add_button.setEnabled(False) - self.existing_button.setEnabled(False) - if is_primary: - self.widget.setEnabled(False) - self.layout = QGridLayout() - self.layout.addWidget(self.label, 0, 0, 1, 5) - self.layout.addWidget(self.widget, 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 update_buttons(self): - if not self.relationship.property.uselist and len(self.data) >= 1: - logger.debug(f"Property {self.relationship} doesn't use list and data is of length: {len(self.data)}") - self.add_button.setEnabled(False) - self.existing_button.setEnabled(False) - else: - self.add_button.setEnabled(True) - self.existing_button.setEnabled(True) - - def parse_row(self, x): - context = {item: x.sibling(x.row(), self.df.columns.get_loc(item)).data() for item in self.df.columns} - try: - object = self.class_object.query(**context) - except KeyError: - object = None - self.widget.doubleClicked.disconnect() - self.add_edit(instance=object) - - def add_new(self, instance: Any = None): - # NOTE: if an existing instance is not being edited, create a new instance - if not instance: - instance = self.class_object() - manager = self.parent().manager - # logger.debug(f"Managers going into add new: {managers}") - dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], manager=manager, add_edit="add") - if dlg.exec(): - new_instance = dlg.parse_form() - # NOTE: My custom __setattr__ should take care of any list problems. - self.parent().instance.__setattr__(self.objectName(), new_instance) - self.parent().update_data() - - def add_existing(self): - dlg = SearchBox(self, object_type=self.class_object, returnable=True, extras=[]) - if dlg.exec(): - rows = dlg.return_selected_rows() - for row in rows: - # logger.debug(f"Querying with {row}") - instance = self.class_object.query(**row) - # NOTE: My custom __setattr__ should take care of any list problems. - self.parent().instance.__setattr__(self.objectName(), instance) - self.parent().update_data() - - def set_data(self) -> None: - """ - sets data in model - """ - logger.debug(f"Self.data: {self.data}") - try: - records = [{k: v['instance_attr'] for k, v in item.omnigui_instance_dict.items()} for item in self.data] - except AttributeError: - records = [] - # logger.debug(f"Records: {records}") - self.df = DataFrame.from_records(records) - try: - self.columns_of_interest = [dict(name=item, column=self.df.columns.get_loc(item)) for item in self.extras] - except (KeyError, AttributeError): - self.columns_of_interest = [] - try: - self.df['id'] = self.df['id'].apply(str) - self.df['id'] = self.df['id'].str.zfill(4) - except KeyError as e: - logger.error(f"Could not alter id to string due to KeyError: {e}") - proxy_model = QSortFilterProxyModel() - proxy_model.setSourceModel(pandasModel(self.df)) - self.widget.setModel(proxy_model) - self.widget.resizeColumnsToContents() - self.widget.resizeRowsToContents() - self.widget.setSortingEnabled(True) - self.widget.doubleClicked.connect(self.parse_row) - self.update_buttons() - - def contextMenuEvent(self, event): - """ - Creates actions for right click menu events. - - Args: - event (_type_): the item of interest - """ - if not self.widget.isEnabled(): - logger.warning(f"{self.objectName()} is disabled.") - return - id = self.widget.selectionModel().currentIndex() - # NOTE: the overly complicated {column_name: row_value} dictionary construction - row_data = {self.df.columns[column]: self.widget.model().index(id.row(), column).data() for column in - range(self.widget.model().columnCount())} - logger.debug(f"Row data: {row_data}") - logger.debug(f"Attempting to grab {self.objectName()} from {self.parent().instance}") - object = getattr(self.parent().instance, self.objectName()) - if isinstance(object, list): - object = next((item for item in object if item.check_all_attributes(attributes=row_data)), None) - logger.debug(f"Object of interest: {object}") - # logger.debug(object) - self.menu = QMenu(self) - try: - remove_action = QAction(f"Remove {object.name}", self) - except AttributeError: - remove_action = QAction(f"Remove object", self) - remove_action.triggered.connect(lambda: self.remove_item(object=object)) - self.menu.addAction(remove_action) - try: - edit_action = QAction(f"Edit {object.name}", self) - except AttributeError: - edit_action = QAction(f"Edit object", self) - edit_action.triggered.connect(lambda: self.add_new(instance=object)) - self.menu.addAction(edit_action) - self.menu.popup(QCursor.pos()) - - def remove_item(self, object): - logger.debug(f"Attempting to remove {object} from {self.parent().instance.__dict__}") - editor = getattr(self.parent().instance, self.objectName().lower()) - logger.debug(f"Editor: {editor}") - if object == self.parent().manager: - logger.error(f"Can't remove manager object.") - return - try: - self.data.remove(object) - except (AttributeError, ValueError) as e: - logger.error(f"Could remove object from self.data due to: {e}") - self.data = [] - try: - logger.debug(f"Using remove technique") - editor.remove(object) - except AttributeError as e: - logger.error(f"Remove failed using set to None for {self.objectName().lower()}.") - setattr(self.parent().instance, self.objectName().lower(), None) - except ValueError as e: - logger.error(f"Remove failed for {self.objectName().lower()} due to {e}.") - self.parent().instance.save() - self.set_data() - - def parse_form(self): - return dict(field=self.objectName(), value=self.data) - - -class JsonEditButton(QWidget): - - def __init__(self, parent, key: str): - super().__init__(parent) - self.setParent(parent) - self.setObjectName(key) - self.addButton = QPushButton("Add Entry", parent=self) - self.viewButton = QPushButton("View >>>", parent=self) - self.layout = QGridLayout() - self.layout.addWidget(self.addButton, 0, 0) - self.layout.addWidget(self.viewButton, 0, 1) - self.setLayout(self.layout) - - -class JsonEditScreen(QDialog): - - def __init__(self, parent, key: str): - super().__init__(parent) - self.class_obj = parent.class_object - self.layout = QGridLayout() - self.setWindowTitle(key) - self.json_field = self.class_obj.json_edit_fields - match self.json_field: - case dict(): - for key, value in self.json_field.items(): - logger.debug(f"Key: {key}, Value: {value}") - row = self.layout.rowCount() - self.layout.addWidget(QLabel(key), row, 0) - match value: - case "int": - self.widget = QSpinBox() - case "str": - self.widget = QLineEdit() - case dict(): - self.widget = DictionaryJsonSubEdit(parent=self, key=key, dic=value) - case _: - continue - self.widget.setObjectName(key) - self.layout.addWidget(self.widget, row, 1) - 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.setLayout(self.layout) - - def parse_form(self): - widgets = [item for item in self.findChildren(QWidget) if item.objectName() in self.json_field.keys()] - logger.debug(f"Widgets: {widgets}") - logger.debug(type(self.json_field)) - if isinstance(self.json_field, dict): - output = {} - elif isinstance(self.json_field, list): - output = [] - else: - raise ValueError(f"Inappropriate data type: {type(self.json_field)}") - for widget in widgets: - logger.debug(f"JsonEditScreen Widget: {widget}") - key = widget.objectName() - match widget: - case QSpinBox(): - value = widget.value() - case QLineEdit(): - value = widget.text() - case DictionaryJsonSubEdit(): - value = widget.parse_form() - case _: - continue - if isinstance(self.json_field, dict): - output[key] = value - elif isinstance(self.json_field, list): - if isinstance(value, list): - output += value - else: - output.append(value) - else: - raise ValueError(f"Inappropriate data type: {type(self.json_field)}") - return output - - -class DictionaryJsonSubEdit(QWidget): - - def __init__(self, parent, key, dic: dict): - super().__init__(parent) - self.layout = QHBoxLayout() - self.setObjectName(key) - self.data = dic - for key, value in self.data.items(): - self.layout.addWidget(QLabel(key)) - match value: - case "int": - self.widget = QSpinBox() - case "str": - self.widget = QLineEdit() - case dict(): - self.widget = DictionaryJsonSubEdit(parent, key=key, dic=value) - self.widget.setObjectName(key) - self.layout.addWidget(self.widget) - self.setLayout(self.layout) - - def parse_form(self): - widgets = [item for item in self.findChildren(QWidget) if item.objectName() in self.data.keys()] - logger.debug(f"Widgets: {widgets}") - output = {} - for widget in widgets: - logger.debug(f"DictionaryJsonSubEdit Widget: {widget}") - key = widget.objectName() - match widget: - case QSpinBox(): - value = widget.value() - case QLineEdit(): - value = widget.text() - case DictionaryJsonSubEdit(): - value = widget.parse_form() - case _: - continue - output[key] = value - return output - - -class LargeTextEdit(QWidget): - - def __init__(self, parent, key: str): - super().__init__(parent) - self.setParent(parent) - self.setObjectName(key) - self.widget = QTextEdit() - self.layout = QVBoxLayout() - self.layout.addWidget(self.widget) - self.setLayout(self.layout) diff --git a/src/submissions/frontend/widgets/omni_manager_pydant.py b/src/submissions/frontend/widgets/omni_manager_pydant.py index cb201d2..2603d18 100644 --- a/src/submissions/frontend/widgets/omni_manager_pydant.py +++ b/src/submissions/frontend/widgets/omni_manager_pydant.py @@ -358,7 +358,6 @@ class EditRelationship(QWidget): obj = getattr(self.parent().omni_object, self.objectName()) if isinstance(obj, list): logger.debug(f"This is a list") - # obj = obj[index] try: # NOTE: Okay, this will not work for editing, since by definition not all attributes will line up. # NOTE: Set items to search by in the Omni object itself? @@ -389,7 +388,7 @@ class EditRelationship(QWidget): """ sets data in model """ - logger.debug(f"Self.data: {self.data}") + # logger.debug(f"Self.data: {self.data}") try: records = [item.to_dataframe_dict() for item in self.data] except AttributeError: diff --git a/src/submissions/frontend/widgets/omni_search.py b/src/submissions/frontend/widgets/omni_search.py index 4b3ab57..4f1d590 100644 --- a/src/submissions/frontend/widgets/omni_search.py +++ b/src/submissions/frontend/widgets/omni_search.py @@ -172,7 +172,7 @@ class SearchResults(QTableView): self.extras = extras + [item for item in deepcopy(self.object_type.searchables)] except AttributeError: self.extras = extras - logger.debug(f"Extras: {self.extras}") + # logger.debug(f"Extras: {self.extras}") def setData(self, df: DataFrame) -> None: """ diff --git a/src/submissions/frontend/widgets/sample_checker.py b/src/submissions/frontend/widgets/sample_checker.py index 102daff..b5760b1 100644 --- a/src/submissions/frontend/widgets/sample_checker.py +++ b/src/submissions/frontend/widgets/sample_checker.py @@ -1,14 +1,11 @@ import logging from pathlib import Path -from typing import List, Generator - +from typing import List from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebEngineWidgets import QWebEngineView -from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, - QDialogButtonBox, QTextEdit, QGridLayout) - +from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout from backend.validators import PydSubmission from tools import get_application_from_parent, jinja_template_loading @@ -16,6 +13,7 @@ env = jinja_template_loading() logger = logging.getLogger(f"submissions.{__name__}") + class SampleChecker(QDialog): def __init__(self, parent, title:str, pyd: PydSubmission): @@ -32,7 +30,6 @@ class SampleChecker(QDialog): self.channel = QWebChannel() self.channel.registerObject('backend', self) # NOTE: Used to maintain javascript functions. - # self.webview.page().setWebChannel(self.channel) template = env.get_template("sample_checker.html") template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) with open(template_path.joinpath("css", "styles.css"), "r") as f: @@ -72,7 +69,3 @@ class SampleChecker(QDialog): sample['color'] = "black" output.append(sample) return output - - - - diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 4a3d500..229b203 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -71,7 +71,7 @@ class SubmissionFormContainer(QWidget): self.setStyleSheet('background-color: light grey;') self.setAcceptDrops(True) # NOTE: if import_drag is emitted, importSubmission will fire - self.import_drag.connect(self.importSubmission) + self.import_drag.connect(lambda fname: self.import_submission_function(fname=fname)) def dragEnterEvent(self, event): """ @@ -90,17 +90,6 @@ class SubmissionFormContainer(QWidget): self.app.last_dir = fname.parent self.import_drag.emit(fname) - @report_result - def importSubmission(self, fname: Path | None = None): - """ - import submission from excel sheet into form - """ - self.app.raise_() - self.app.activateWindow() - report = Report() - self.import_submission_function(fname) - return report - @report_result def import_submission_function(self, fname: Path | None = None) -> Report: """ @@ -112,6 +101,8 @@ class SubmissionFormContainer(QWidget): Returns: Report: Object to give results of import. """ + self.app.raise_() + self.app.activateWindow() logger.info(f"\n\nStarting Import...\n\n") report = Report() # NOTE: Clear any previous forms. @@ -436,7 +427,7 @@ class SubmissionFormWidget(QWidget): if field is not None: info[field] = value self.pyd.reagents = reagents - logger.debug(f"Reagents from form: {reagents}") + # logger.debug(f"Reagents from form: {reagents}") for item in self.recover: if hasattr(self, item): value = getattr(self, item) @@ -446,6 +437,7 @@ class SubmissionFormWidget(QWidget): report.add_result(report) return report + class InfoItem(QWidget): def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None, @@ -691,7 +683,6 @@ class SubmissionFormWidget(QWidget): if new: dlg = QuestionAsker(title=f"Add {lot}?", 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(instance=wanted_reagent) return wanted_reagent, report diff --git a/src/submissions/frontend/widgets/summary.py b/src/submissions/frontend/widgets/summary.py index 4e44f67..0df5aef 100644 --- a/src/submissions/frontend/widgets/summary.py +++ b/src/submissions/frontend/widgets/summary.py @@ -40,7 +40,7 @@ class Summary(InfoPane): None """ super().update_data() - orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)] + orgs = self.org_select.get_checked() self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs) self.webview.setHtml(self.report_obj.html) if self.report_obj.subs: diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index c508f54..b75b98f 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -27,6 +27,7 @@ from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError from pytz import timezone as tz from functools import wraps + timezone = tz("America/Winnipeg") logger = logging.getLogger(f"submissions.{__name__}") @@ -248,7 +249,6 @@ def timer(func): func (__function__): incoming function """ - @wraps(func) def wrapper(*args, **kwargs): start_time = time.perf_counter() @@ -257,7 +257,6 @@ def timer(func): run_time = end_time - start_time print(f"Finished {func.__name__}() in {run_time:.4f} secs") return value - return wrapper @@ -483,12 +482,10 @@ def setup_lookup(func): elif v is not None: sanitized_kwargs[k] = v return func(*args, **sanitized_kwargs) - return wrapper def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bool]: - # for manager in managers: if manager is None: return None, False # logger.debug(f"Manager: {manager}, aliases: {manager.aliases}, Key: {object_name}") @@ -535,6 +532,7 @@ def get_application_from_parent(widget): class Result(BaseModel, arbitrary_types_allowed=True): + owner: str = Field(default="", validate_default=True) code: int = Field(default=0) msg: str | Exception @@ -639,7 +637,6 @@ def rreplace(s: str, old: str, new: str) -> str: def list_sort_dict(input_dict: dict, sort_list: list) -> dict: - # sort_list.reverse() sort_list = reversed(sort_list) for item in sort_list: try: @@ -661,7 +658,10 @@ def remove_key_from_list_of_dicts(input_list: list, key: str) -> list: list: List of updated dictionaries """ for item in input_list: - del item[key] + try: + del item[key] + except KeyError: + continue return input_list @@ -688,6 +688,7 @@ def super_splitter(ins_str: str, substring: str, idx: int) -> str: try: return ins_str.split(substring)[idx] except IndexError: + logger.error(f"Index of split {idx} not found.") return ins_str @@ -767,7 +768,6 @@ def under_development(func): Result(owner=func.__str__(), code=1, msg=error_msg, status="warning")) return report - return wrapper @@ -856,7 +856,6 @@ def create_holidays_for_year(year: int | None = None) -> List[date]: offset = -d.weekday() # weekday == 0 means Monday output = d + timedelta(offset) return output.date() - if not year: year = date.today().year # NOTE: Includes New Year's day for next year. @@ -886,7 +885,7 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) - Returns: bool: True if dicto is equal to any dictionary in the list. """ - logger.debug(f"Comparing: {listo} and {dicto}") + # logger.debug(f"Comparing: {listo} and {dicto}") if isinstance(dicto, list) and isinstance(listo, list): return listo == dicto elif isinstance(dicto, dict) and isinstance(listo, dict): @@ -957,7 +956,6 @@ class Settings(BaseSettings, extra="allow"): settings_path = None if settings_path is None: # NOTE: Check user .config/submissions directory - # if CONFIGDIR.joinpath("config.yml").exists(): if cls.configdir.joinpath("config.yml").exists(): settings_path = cls.configdir.joinpath("config.yml") # NOTE: Check user .submissions directory @@ -969,8 +967,6 @@ class Settings(BaseSettings, extra="allow"): settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml") else: settings_path = project_path.joinpath('src', 'config.yml') - # with open(settings_path, "r") as dset: - # default_settings = yaml.load(dset, Loader=yaml.Loader) else: # NOTE: check if user defined path is directory if settings_path.is_dir(): @@ -1285,3 +1281,4 @@ class Settings(BaseSettings, extra="allow"): ctx = Settings() +