From 8a16738e937f952f4a36b39a598892deb7578ab6 Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 9 May 2025 14:35:53 -0500 Subject: [PATCH] placeholder --- src/submissions/backend/db/models/__init__.py | 17 +++- .../backend/db/models/submissions.py | 27 +++++- src/submissions/backend/excel/reports.py | 2 +- src/submissions/backend/validators/pydant.py | 95 ++++++++++++++++++- .../frontend/widgets/submission_table.py | 68 ++++++++++--- .../frontend/widgets/submission_widget.py | 50 +++++++++- 6 files changed, 225 insertions(+), 34 deletions(-) diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index ddc0355..aba2fee 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -389,13 +389,13 @@ class BaseClass(Base): except AttributeError: return super().__setattr__(key, value) if isinstance(field_type, InstrumentedAttribute): - # logger.debug(f"{key} is an InstrumentedAttribute.") + logger.debug(f"{key} is an InstrumentedAttribute.") match field_type.property: case ColumnProperty(): - # logger.debug(f"Setting ColumnProperty to {value}") + logger.debug(f"Setting ColumnProperty to {value}") return super().__setattr__(key, value) case _RelationshipDeclared(): - # logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}") + logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}") if field_type.property.uselist: logger.debug(f"Setting with uselist") existing = self.__getattribute__(key) @@ -409,7 +409,8 @@ class BaseClass(Base): value = existing + [value] else: if isinstance(value, list): - value = value + # value = value + pass else: value = [value] value = list(set(value)) @@ -421,7 +422,13 @@ class BaseClass(Base): value = value[0] else: raise ValueError("Object is too long to parse a single value.") - return super().__setattr__(key, value) + try: + return super().__setattr__(key, value) + except AttributeError: + logger.debug(f"Possible attempt to set relationship to simple var type.") + relationship_class = field_type.property.entity.entity + value = relationship_class.query(name=value) + return super().__setattr__(key, value) case _: return super().__setattr__(key, value) else: diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 5eae92a..f5542c4 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -46,7 +46,7 @@ class ClientSubmission(BaseClass, LogMixin): """ id = Column(INTEGER, primary_key=True) #: primary key - submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab + submitter_plate_id = Column(String(127), unique=True) #: The number given to the submission by the submitting lab submitted_date = Column(TIMESTAMP) #: Date submission received submitting_lab = relationship("Organization", back_populates="submissions") #: client org submitting_lab_id = Column(INTEGER, ForeignKey("_organization.id", ondelete="SET NULL", @@ -56,7 +56,7 @@ class ClientSubmission(BaseClass, LogMixin): sample_count = Column(INTEGER) #: Number of samples in the submission comment = Column(JSON) runs = relationship("BasicSubmission", back_populates="client_submission") #: many-to-one relationship - + misc_info = Column(JSON) contact = relationship("Contact", back_populates="submissions") #: client org contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL", name="fk_BS_contact_id")) #: client lab id from _organizations @@ -92,6 +92,16 @@ class ClientSubmission(BaseClass, LogMixin): except AttributeError: self._submission_category = "NA" + def __init__(self): + super().__init__() + self.misc_info = {} + + def set_attribute(self, key, value): + if hasattr(self, key): + super().__setattr__(key, value) + else: + self.misc_info[key] = value + @classmethod def recruit_parser(cls): pass @@ -237,7 +247,7 @@ class ClientSubmission(BaseClass, LogMixin): output = { "id": self.id, "submission_type": self.submission_type_name, - "submitter_plate_number": self.submitter_plate_num, + "submitter_plate_number": self.submitter_plate_id, "submitted_date": self.submitted_date.strftime("%Y-%m-%d"), "submitting_lab": sub_lab, "sample_count": self.sample_count, @@ -279,11 +289,14 @@ class ClientSubmission(BaseClass, LogMixin): output["runs"] = runs return output + class BasicSubmission(BaseClass, LogMixin): """ Object for an entire submission run. Links to client submissions, reagents, equipment, processes """ + + id = Column(INTEGER, primary_key=True) #: primary key rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012) client_submission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL", @@ -384,6 +397,10 @@ class BasicSubmission(BaseClass, LogMixin): def organization(self): return self.submitting_lab + @hybrid_property + def name(self): + return self.rsl_plate_num + @classproperty def jsons(cls) -> List[str]: """ @@ -567,8 +584,8 @@ class BasicSubmission(BaseClass, LogMixin): "id": self.id, "plate_number": self.rsl_plate_num, "submission_type": self.client_submission.submission_type_name, - "submitter_plate_number": self.client_submission.submitter_plate_num, - "submitted_date": self.client_submission.submitted_date.strftime("%Y-%m-%d"), + "submitter_plate_number": self.client_submission.submitter_plate_id, + "started_date": self.client_submission.submitted_date.strftime("%Y-%m-%d"), "submitting_lab": sub_lab, "sample_count": self.client_submission.sample_count, "extraction_kit": "Change submissions.py line 388", diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 708e33a..0d526b7 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -68,7 +68,7 @@ class ReportMaker(object): {'extraction_kit': 'count', 'cost': 'sum', 'sample_count': 'sum'}) df2 = df2.rename(columns={"extraction_kit": 'run_count'}) df = df.drop('id', axis=1) - df = df.sort_values(['submitting_lab', "submitted_date"]) + df = df.sort_values(['submitting_lab', "started_date"]) return df, df2 def make_report_html(self, df: DataFrame) -> str: diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 6852f6c..0456cc5 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -384,7 +384,6 @@ class PydSubmission(BaseModel, extra='allow'): filepath: Path submission_type: dict | None submitter_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) - submitted_date: dict | None rsl_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) submitted_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True) submitting_lab: dict | None @@ -1331,21 +1330,93 @@ class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True): # NOTE: Generified objects below: -class PydClientSubmission(BaseModel, extra="allow"): +class PydClientSubmission(BaseModel, extra="allow", validate_assignment=True): + + sql_object: ClassVar = ClientSubmission filepath: Path submission_type: dict | None - submitter_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) - submitted_date: dict | None submitted_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True) submitting_lab: dict | None sample_count: dict | None - kittype: dict | None submission_category: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) comment: dict | None = Field(default=dict(value="", missing=True), validate_default=True) cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) contact: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) + submitter_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) + @field_validator("sample_count") + @classmethod + def enforce_integer(cls, value): + try: + value['value'] = int(value['value']) + except ValueError: + raise f"sample count value must be an integer" + return value + + @field_validator("submitter_plate_num") + @classmethod + def create_submitter_plate_num(cls, value, values): + if value['value'] in [None, "None"]: + val = f"{values.data['submission_type']['value']}-{values.data['submission_category']['value']}-{values.data['submitted_date']['value']}" + return dict(value=val, missing=True) + else: + value['value'] = value['value'].strip() + return value + + @field_validator("submitted_date") + @classmethod + def rescue_date(cls, value): + try: + check = value['value'] is None + except TypeError: + check = True + if check: + return dict(value=date.today(), missing=True) + return value + + def filter_field(self, key: str) -> Any: + """ + Attempts to get value from field dictionary + + Args: + key (str): name of the field of interest + + Returns: + Any (): Value found. + """ + item = getattr(self, key) + match item: + case dict(): + try: + item = item['value'] + except KeyError: + logger.error(f"Couldn't get dict value: {item}") + case _: + pass + return item + + def improved_dict(self, dictionaries: bool = True) -> dict: + """ + Adds model_extra to fields. + + Args: + dictionaries (bool, optional): Are dictionaries expected as input? i.e. Should key['value'] be retrieved. Defaults to True. + + Returns: + dict: This instance as a dictionary + """ + fields = list(self.model_fields.keys()) + list(self.model_extra.keys()) + if dictionaries: + output = {k: getattr(self, k) for k in fields} + else: + output = {k: self.filter_field(k) for k in fields} + try: + del output['filepath'] + except KeyError: + pass + logger.debug(f"Output; {pformat(output)}") + return output def to_form(self, parent: QWidget, disable: list | None = None): """ @@ -1360,3 +1431,17 @@ class PydClientSubmission(BaseModel, extra="allow"): """ from frontend.widgets.submission_widget import ClientSubmissionFormWidget return ClientSubmissionFormWidget(parent=parent, submission=self, disable=disable) + + def to_sql(self): + sql = self.sql_object() + for key, value in self.improved_dict().items(): + if isinstance(value, dict): + value = value['value'] + # if hasattr(sql, key): + # try: + sql.set_attribute(key, value) + # except AttributeError: + # continue + # else: + # sql.misc_info[key] = value + print(sql.__dict__) diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 8128e39..6e1a2f3 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -5,7 +5,7 @@ import logging import sys from pprint import pformat from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \ - QHeaderView, QAbstractItemView + QHeaderView, QAbstractItemView, QWidget from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor from backend.db.models import BasicSubmission, ClientSubmission @@ -230,8 +230,12 @@ class SubmissionsSheet(QTableView): class RunDelegate(QStyledItemDelegate): def __init__(self, parent=None): super(RunDelegate, self).__init__(parent) - self._plus_icon = QIcon("plus.png") - self._minus_icon = QIcon("minus.png") + pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton + icon1 = QWidget().style().standardIcon(pixmapi) + pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton + icon2 = QWidget().style().standardIcon(pixmapi) + self._plus_icon = icon1 + self._minus_icon = icon2 def initStyleOption(self, option, index): super(RunDelegate, self).initStyleOption(option, index) @@ -258,6 +262,11 @@ class SubmissionsTree(QTreeView): self.setSelectionBehavior(QAbstractItemView.selectionBehavior(self).SelectRows) # self.setStyleSheet("background-color: #0D1225;") self.set_data() + self.doubleClicked.connect(self.show_details) + + for ii in range(2): + self.resizeColumnToContents(ii) + @pyqtSlot(QModelIndex) def on_clicked(self, index): @@ -273,9 +282,20 @@ class SubmissionsTree(QTreeView): logger.debug(pformat(self.data)) # sys.exit() for submission in self.data: - group_item = self.model.add_group(submission['submitter_plate_number']) + group_str = f"{submission['submission_type']}-{submission['submitter_plate_number']}-{submission['submitted_date']}" + group_item = self.model.add_group(group_str) for run in submission['runs']: - self.model.append_element_to_group(group_item=group_item, texts=run['plate_number']) + self.model.append_element_to_group(group_item=group_item, element=run) + + def show_details(self, sel: QModelIndex): + id = self.selectionModel().currentIndex() + # NOTE: Convert to data in id column (i.e. column 0) + id = id.sibling(id.row(), 1) + try: + id = int(id.data()) + except ValueError: + return + BasicSubmission.query(id=id).show_details(self) def link_extractions(self): @@ -286,12 +306,19 @@ class SubmissionsTree(QTreeView): class ClientRunModel(QStandardItemModel): + def __init__(self, parent=None): super(ClientRunModel, self).__init__(parent) - self.setColumnCount(8) - self.setHorizontalHeaderLabels(["id", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""]) + headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Technician", "Signed By"] + self.setColumnCount(len(headers)) + self.setHorizontalHeaderLabels(headers) + for i in range(self.columnCount()): it = self.horizontalHeaderItem(i) + try: + logger.debug(it.text()) + except AttributeError: + pass # it.setForeground(QColor("#F2F2F2")) def add_group(self, group_name): @@ -313,18 +340,29 @@ class ClientRunModel(QStandardItemModel): # it.setForeground(QColor("#F2F2F2")) return item_root - def append_element_to_group(self, group_item, texts): + def append_element_to_group(self, group_item, element:dict): + logger.debug(f"Element: {pformat(element)}") j = group_item.rowCount() item_icon = QStandardItem() item_icon.setEditable(False) - item_icon.setIcon(QIcon("game.png")) + # item_icon.setBackground(QColor("#0D1225")) - group_item.setChild(j, 0, item_icon) - for i, text in enumerate(texts): - item = QStandardItem(text) + # group_item.setChild(j, 0, item_icon) + for i in range(self.columnCount()): + it = self.horizontalHeaderItem(i) + try: + key = it.text().lower().replace(" ", "_") + except AttributeError: + continue + if not key: + continue + logger.debug(f"Looking for {key} in column {i}") + value = str(element[key]) + logger.debug(f"Got value: {value}") + item = QStandardItem(value) + item.setBackground(QColor("#CFE2F3")) item.setEditable(False) - # item.setBackground(QColor("#0D1225")) - # item.setForeground(QColor("#F2F2F2")) - group_item.setChild(j, i+1, item) + group_item.setChild(j, i, item) + # group_item.setChild(j, 1, QStandardItem("B")) diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index e91d740..fbadd4f 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -438,7 +438,7 @@ class SubmissionFormWidget(QWidget): value = getattr(self, item) info[item] = value for k, v in info.items(): - self.pyd.set_attribute(key=k, value=v) + self.pyd.__setattr__(k, v) report.add_result(report) return report @@ -786,9 +786,53 @@ class ClientSubmissionFormWidget(SubmissionFormWidget): def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None: super().__init__(parent, submission=submission, disable=disable) save_btn = QPushButton("Save") - start_run_btn = QPushButton("Save && Start Run") + start_run_btn = QPushButton("Save && Add Run") self.layout.addWidget(save_btn) self.layout.addWidget(start_run_btn) - + start_run_btn.clicked.connect(self.create_new_submission) + del self.disabler + + def parse_form(self) -> Report: + """ + Transforms form info into PydSubmission + + Returns: + Report: Report on status of parse. + """ + report = Report() + logger.info(f"Hello from client submission form parser!") + info = {} + reagents = [] + for widget in self.findChildren(QWidget): + match widget: + case self.ReagentFormWidget(): + reagent = widget.parse_form() + if reagent is not None: + reagents.append(reagent) + else: + report.add_result(Result(msg="Failed integrity check", status="Critical")) + return report + case self.InfoItem(): + field, value = widget.parse_form() + if field is not None: + info[field] = value + # logger.debug(f"Reagents from form: {reagents}") + for item in self.recover: + if hasattr(self, item): + value = getattr(self, item) + info[item] = value + for k, v in info.items(): + self.pyd.__setattr__(k, v) + report.add_result(report) + return report + + @report_result + def to_pydantic(self, *args): + self.parse_form() + return self.pyd + + def create_new_submission(self, *args) -> Report: + self.parse_form() + sql = self.pyd.to_sql()