diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index e6936c5..e496e6f 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -14,7 +14,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property from datetime import date, datetime, timedelta from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \ - jinja_template_loading + jinja_template_loading, ctx from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING from pandas import ExcelFile from pathlib import Path @@ -1053,6 +1053,7 @@ class ProcedureType(BaseClass): reagent_map = Column(JSON) plate_columns = Column(INTEGER, default=0) plate_rows = Column(INTEGER, default=0) + allowed_result_methods = Column(JSON) procedure = relationship("Procedure", back_populates="proceduretype") #: Concrete control of this type. @@ -1095,6 +1096,10 @@ class ProcedureType(BaseClass): cascade="all, delete-orphan" ) #: Association of tiproles + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.allowed_result_methods = dict() + def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]: """ Make a map of all locations for tips or equipment. @@ -1223,9 +1228,10 @@ class ProcedureType(BaseClass): class Procedure(BaseClass): id = Column(INTEGER, primary_key=True) - _name = Column(String, unique=True) + name = Column(String, unique=True) repeat = Column(INTEGER, nullable=False) - technician = Column(JSON) #: name of processing tech(s) + technician = Column(String(64)) #: name of processing tech(s) + results = relationship("Results", back_populates="procedure", uselist=True) proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL", name="fk_PRO_proceduretype_id")) #: client lab id from _organizations)) proceduretype = relationship("ProcedureType", back_populates="procedure") @@ -1274,14 +1280,6 @@ class Procedure(BaseClass): tips = association_proxy("proceduretipsassociation", "tips") - @hybrid_property - def name(self): - return f"{self.proceduretype.name}-{self.run.rsl_plate_num}" - - @name.setter - def name(self, value): - self._name = value - @validates('repeat') def validate_repeat(self, key, value): if value > 1: @@ -1314,6 +1312,33 @@ class Procedure(BaseClass): output['name'] = self.name return output + @property + def custom_context_events(self) -> dict: + """ + Creates dictionary of str:function to be passed to context menu + + Returns: + dict: dictionary of functions + """ + names = ["Add Results", "Edit", "Add Comment", "Show Details", "Delete"] + return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names} + + def add_results(self, obj, resultstype_name:str): + logger.debug(f"Add Results! {resultstype_name}") + + def edit(self, obj): + logger.debug("Edit!") + + def add_comment(self, obj): + logger.debug("Add Comment!") + + def show_details(self, obj): + logger.debug("Show Details!") + + def delete(self, obj): + logger.debug("Delete!") + + class ProcedureTypeKitTypeAssociation(BaseClass): @@ -2634,3 +2659,33 @@ class ProcedureTipsAssociation(BaseClass): return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name) +class Results(BaseClass): + + id = Column(INTEGER, primary_key=True) + result = Column(JSON) + procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL', + name="fk_RES_procedure_id")) + procedure = relationship("Procedure", back_populates="results") + assoc_id = Column(INTEGER, ForeignKey("_proceduresampleassociation.id", ondelete='SET NULL', + name="fk_RES_ASSOC_id")) + + sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results") + _img = Column(String(128)) + + @property + def image(self) -> bytes: + dir = self.__directory_path__.joinpath("submission_imgs.zip") + try: + assert dir.exists() + except AssertionError: + raise FileNotFoundError(f"{dir} not found.") + logger.debug(f"Getting image from {self.__directory_path__}") + with zipfile.ZipFile(dir) as zf: + with zf.open(self._img) as f: + return f.read() + + @image.setter + def image(self, value): + self._img = value + + diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index cc8fc33..81573b0 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -16,7 +16,7 @@ from pprint import pformat from pandas import DataFrame from sqlalchemy.ext.hybrid import hybrid_property from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin, Procedure, kittype_procedure -from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table +from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table, Sequence from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.ext.associationproxy import association_proxy @@ -2028,6 +2028,8 @@ class RunSampleAssociation(BaseClass): class ProcedureSampleAssociation(BaseClass): + + id = Column(INTEGER, primary_key=True) procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated equipment row = Column(INTEGER) @@ -2038,3 +2040,29 @@ class ProcedureSampleAssociation(BaseClass): sample = relationship(Sample, back_populates="sampleprocedureassociation") #: associated equipment + results = relationship("Results", back_populates="sampleprocedureassociation") + + + + # def __init__(self, new_id:int|None=None, **kwarg): + # if new_id: + # self.id = new_id + # else: + # self.id = self.__class__.autoincrement_id() + # # new_id = self.__class__.autoincrement_id() + # super().__init__(**kwarg) + + + @classmethod + def autoincrement_id(cls) -> int: + """ + Increments the association id automatically + + Returns: + int: incremented id + """ + try: + return max([item.id for item in cls.query()]) + 1 + except ValueError as e: + logger.error(f"Problem incrementing id: {e}") + return 1 diff --git a/src/submissions/backend/excel/parsers/__init__.py b/src/submissions/backend/excel/parsers/__init__.py index c19e1b4..6263e1d 100644 --- a/src/submissions/backend/excel/parsers/__init__.py +++ b/src/submissions/backend/excel/parsers/__init__.py @@ -1,26 +1,25 @@ """ """ +import logging, re from pathlib import Path +from typing import Generator, Tuple from openpyxl import load_workbook +from pandas import DataFrame from backend.validators import pydant +logger = logging.getLogger(f"submissions.{__name__}") + class DefaultParser(object): - - default_range_dict = dict( - start_row=2, - end_row=18, - key_column=1, - value_column=2, - sheet="Sample List" - ) - def __repr__(self): return f"{self.__class__.__name__}<{self.filepath.stem}>" - def __init__(self, filepath: Path | str, range_dict: dict | None = None): - self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}") + def __init__(self, filepath: Path | str, range_dict: dict | None = None, *args, **kwargs): + try: + self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}") + except AttributeError: + self._pyd_object = pydant.PydResults if isinstance(filepath, str): self.filepath = Path(filepath) else: @@ -30,5 +29,63 @@ class DefaultParser(object): self.range_dict = self.__class__.default_range_dict else: self.range_dict = range_dict + for item in self.range_dict: + item['worksheet'] = self.workbook[item['sheet']] -from .submission_parser import * \ No newline at end of file + def to_pydantic(self): + data = {key: value for key, value in self.parsed_info} + data['filepath'] = self.filepath + return self._pyd_object(**data) + + +class DefaultKEYVALUEParser(DefaultParser): + + default_range_dict = [dict( + start_row=2, + end_row=18, + key_column=1, + value_column=2, + sheet="Sample List" + )] + + + + @property + def parsed_info(self) -> Generator[Tuple, None, None]: + for item in self.range_dict: + rows = range(item['start_row'], item['end_row'] + 1) + for row in rows: + key = item['worksheet'].cell(row, item['key_column']).value + if key: + # Note: Remove anything in brackets. + key = re.sub(r"\(.*\)", "", key) + key = key.lower().replace(":", "").strip().replace(" ", "_") + value = item['worksheet'].cell(row, item['value_column']).value + value = dict(value=value, missing=False if value else True) + yield key, value + + +class DefaultTABLEParser(DefaultParser): + + default_range_dict = [dict( + header_row=20, + end_row=116, + sheet="Sample List" + )] + + @property + def parsed_info(self): + for item in self.range_dict: + list_worksheet = self.workbook[item['sheet']] + list_df = DataFrame([item for item in list_worksheet.values][item['header_row'] - 1:]) + list_df.columns = list_df.iloc[0] + list_df = list_df[1:] + list_df = list_df.dropna(axis=1, how='all') + for ii, row in enumerate(list_df.iterrows()): + output = {key.lower().replace(" ", "_"): value for key, value in row[1].to_dict().items()} + yield output + + def to_pydantic(self, **kwargs): + return [self._pyd_object(**output) for output in self.parsed_info] + +from .submission_parser import * diff --git a/src/submissions/backend/excel/parsers/pcr_parser.py b/src/submissions/backend/excel/parsers/pcr_parser.py new file mode 100644 index 0000000..427aaf5 --- /dev/null +++ b/src/submissions/backend/excel/parsers/pcr_parser.py @@ -0,0 +1,99 @@ +""" + +""" +import logging, re, sys +from pprint import pformat +from pathlib import Path +from typing import Generator, Tuple + +from openpyxl import load_workbook + +from backend.db.models import Run, Sample +from . import DefaultKEYVALUEParser, DefaultTABLEParser + +logger = logging.getLogger(f"submissions.{__name__}") + + +class PCRSampleParser(DefaultTABLEParser): + """Object to pull data from Design and Analysis PCR export file.""" + + default_range_dict = [dict( + header_row=25, + sheet="Results" + )] + + @property + def parsed_info(self): + output = [item for item in super().parsed_info] + merge_column = "sample" + sample_names = list(set([item['sample'] for item in output])) + for sample in sample_names: + multi = dict() + sois = (item for item in output if item['sample']==sample) + for soi in sois: + multi[soi['target']] = {k:v for k, v in soi.items() if k != "target"} + yield (sample, multi) + + def to_pydantic(self): + for key, sample_info in self.parsed_info: + sample_obj = Sample.query(sample_id=key) + if sample_obj and not isinstance(sample_obj, list): + yield self._pyd_object(results=sample_info, parent=sample_obj) + else: + continue + + + +class PCRInfoParser(DefaultKEYVALUEParser): + + default_range_dict = [dict( + start_row=1, + end_row=24, + key_column=1, + value_column=2, + sheet="Results" + )] + + # def __init__(self, filepath: Path | str, range_dict: dict | None = None): + # super().__init__(filepath=filepath, range_dict=range_dict) + # self.worksheet = self.workbook[self.range_dict['sheet']] + # self.rows = range(self.range_dict['start_row'], self.range_dict['end_row'] + 1) + # + # @property + # def parsed_info(self) -> Generator[Tuple, None, None]: + # for row in self.rows: + # key = self.worksheet.cell(row, self.range_dict['key_column']).value + # if key: + # key = re.sub(r"\(.*\)", "", key) + # key = key.lower().replace(":", "").strip().replace(" ", "_") + # value = self.worksheet.cell(row, self.range_dict['value_column']).value + # value = dict(value=value, missing=False if value else True) + # yield key, value + # + + def to_pydantic(self): + from backend.db.models import Procedure + data = {key: value for key, value in self.parsed_info} + data['filepath'] = self.filepath + return self._pyd_object(**data, parent=Procedure) + + + # @property + # def pcr_info(self) -> dict: + # """ + # Parse general info rows for all types of PCR results + # """ + # info_map = self.submission_obj.get_submission_type().sample_map['pcr_general_info'] + # sheet = self.xl[info_map['sheet']] + # iter_rows = sheet.iter_rows(min_row=info_map['start_row'], max_row=info_map['end_row']) + # pcr = {} + # for row in iter_rows: + # try: + # key = row[0].value.lower().replace(' ', '_') + # except AttributeError as e: + # logger.error(f"No key: {row[0].value} due to {e}") + # continue + # value = row[1].value or "" + # pcr[key] = value + # pcr['imported_by'] = getuser() + # return pcr diff --git a/src/submissions/backend/excel/parsers/submission_parser.py b/src/submissions/backend/excel/parsers/submission_parser.py index 832a724..12e8ae9 100644 --- a/src/submissions/backend/excel/parsers/submission_parser.py +++ b/src/submissions/backend/excel/parsers/submission_parser.py @@ -1,66 +1,50 @@ """ """ -import logging, re -from pathlib import Path -from typing import Generator, Tuple -from pandas import DataFrame - -from . import DefaultParser +import logging +from string import ascii_lowercase +from typing import Generator +from tools import row_keys +from . import DefaultKEYVALUEParser, DefaultTABLEParser logger = logging.getLogger(f"submissions.{__name__}") -class ClientSubmissionParser(DefaultParser): +class ClientSubmissionParser(DefaultKEYVALUEParser): """ Object for retrieving submitter info from "sample list" sheet """ - def __init__(self, filepath: Path | str, range_dict: dict | None = None): - super().__init__(filepath=filepath, range_dict=range_dict) - self.worksheet = self.workbook[self.range_dict['sheet']] - self.rows = range(self.range_dict['start_row'], self.range_dict['end_row'] + 1) - - @property - def parsed_info(self) -> Generator[Tuple, None, None]: - for row in self.rows: - key = self.worksheet.cell(row, self.range_dict['key_column']).value - if key: - key = re.sub(r"\(.*\)", "", key) - key = key.lower().replace(":", "").strip().replace(" ", "_") - value = self.worksheet.cell(row, self.range_dict['value_column']).value - value = dict(value=value, missing=False if value else True) - yield key, value - - def to_pydantic(self): - data = {key: value for key, value in self.parsed_info} - data['filepath'] = self.filepath - return self._pyd_object(**data) + default_range_dict = [dict( + start_row=2, + end_row=18, + key_column=1, + value_column=2, + sheet="Sample List" + )] -class SampleParser(DefaultParser): + +class SampleParser(DefaultTABLEParser): """ Object for retrieving submitter info from "sample list" sheet """ - default_range_dict = dict( + default_range_dict = [dict( header_row=20, end_row=116, - list_sheet="Sample List" - ) - - def __init__(self, filepath: Path | str, range_dict: dict | None = None): - super().__init__(filepath=filepath, range_dict=range_dict) - self.list_worksheet = self.workbook[self.range_dict['list_sheet']] - self.list_df = DataFrame([item for item in self.list_worksheet.values][self.range_dict['header_row'] - 1:]) - self.list_df.columns = self.list_df.iloc[0] - self.list_df = self.list_df[1:] - self.list_df = self.list_df.dropna(axis=1, how='all') + sheet="Sample List" + )] @property def parsed_info(self) -> Generator[dict, None, None]: - for ii, row in enumerate(self.list_df.iterrows()): - sample = {key.lower().replace(" ", "_"): value for key, value in row[1].to_dict().items()} + output = super().parsed_info + for ii, sample in enumerate(output): + if isinstance(sample["row"], str) and sample["row"].lower() in ascii_lowercase[0:8]: + try: + sample["row"] = row_keys[sample["row"]] + except KeyError: + pass sample['submission_rank'] = ii + 1 yield sample diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index a07ef8e..fff0056 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -208,4 +208,4 @@ class RSLNamer(object): from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ - PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure + PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index bf2e58f..11cbb1f 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -780,7 +780,7 @@ class PydSubmission(BaseModel, extra='allow'): return missing_info, missing_reagents @report_result - def to_sql(self) -> Tuple[BasicRun | None, Report]: + def to_sql(self) -> Tuple[Run | None, Report]: """ Converts this instance into a backend.db.models.procedure.BasicRun instance @@ -791,7 +791,7 @@ class PydSubmission(BaseModel, extra='allow'): dicto = self.improved_dict() # logger.debug(f"Pydantic procedure type: {self.proceduretype['value']}") # logger.debug(f"Pydantic improved_dict: {pformat(dicto)}") - instance, result = BasicRun.query_or_create(submissiontype=self.submission_type['value'], + instance, result = Run.query_or_create(submissiontype=self.submission_type['value'], rsl_plate_num=self.rsl_plate_num['value']) # logger.debug(f"Created or queried instance: {instance}") if instance is None: @@ -1353,7 +1353,15 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): def rescue_name(cls, value, values): if value['value'] == cls.model_fields['name'].default['value']: if values.data['proceduretype']: - value['value'] = values.data['proceduretype'].name + procedure_type = values.data['proceduretype'].name + else: + procedure_type = None + if values.data['run']: + run = values.data['run'].rsl_plate_num + else: + run = None + value['value'] = f"{procedure_type}-{run}" + value['missing'] = True return value @field_validator("possible_kits") @@ -1522,3 +1530,10 @@ class PydClientSubmission(PydBaseClass): """ from frontend.widgets.submission_widget import ClientSubmissionFormWidget return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable) + + +class PydResults(PydBaseClass, arbitrary_types_allowed=True): + + results: dict = Field(default={}) + parent: Sample|Procedure + diff --git a/src/submissions/frontend/widgets/results/PCR.py b/src/submissions/frontend/widgets/results/PCR.py new file mode 100644 index 0000000..50fbe03 --- /dev/null +++ b/src/submissions/frontend/widgets/results/PCR.py @@ -0,0 +1,26 @@ +""" + +""" +from pathlib import Path +from backend.validators import PydResults +from backend.db.models import Procedure, Results +from backend.excel.parsers.pcr_parser import PCRSampleParser, PCRInfoParser +from frontend.widgets.functions import select_open_file +from . import DefaultResults + +class PCR(DefaultResults): + + def __init__(self, procedure: Procedure, fname:Path|str|None=None): + self.procedure = procedure + if not fname: + self.fname = select_open_file(file_extension="xlsx") + elif isinstance(fname, str): + self.fname = Path(fname) + self.info_parser = PCRInfoParser(filepath=fname) + self.sample_parser = PCRSampleParser(filepath=fname) + + def build_procedure(self): + results = PydResults(parent=self.procedure) + results.results = + + diff --git a/src/submissions/frontend/widgets/results/__init__.py b/src/submissions/frontend/widgets/results/__init__.py new file mode 100644 index 0000000..12dc63a --- /dev/null +++ b/src/submissions/frontend/widgets/results/__init__.py @@ -0,0 +1,7 @@ + + +class DefaultResults(object): + + pass + +from .PCR import pcr \ No newline at end of file diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index f327af1..3c69558 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -231,27 +231,27 @@ class SubmissionsSheet(QTableView): return report -class ClientSubmissionDelegate(QStyledItemDelegate): - - def __init__(self, parent=None): - super(ClientSubmissionDelegate, self).__init__(parent) - 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(ClientSubmissionDelegate, self).initStyleOption(option, index) - if not index.parent().isValid(): - is_open = bool(option.state & QStyle.StateFlag.State_Open) - option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration - option.icon = self._minus_icon if is_open else self._plus_icon +# class ClientSubmissionDelegate(QStyledItemDelegate): +# +# def __init__(self, parent=None): +# super(ClientSubmissionDelegate, self).__init__(parent) +# 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(ClientSubmissionDelegate, self).initStyleOption(option, index) +# if not index.parent().isValid(): +# is_open = bool(option.state & QStyle.StateFlag.State_Open) +# option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration +# option.icon = self._minus_icon if is_open else self._plus_icon -class RunDelegate(ClientSubmissionDelegate): - pass +# class RunDelegate(ClientSubmissionDelegate): +# pass class SubmissionsTree(QTreeView): @@ -262,11 +262,11 @@ class SubmissionsTree(QTreeView): def __init__(self, model, parent=None): super(SubmissionsTree, self).__init__(parent) self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count() - self.setIndentation(0) + # self.setIndentation(0) self.setExpandsOnDoubleClick(False) - self.clicked.connect(self.on_clicked) - delegate1 = ClientSubmissionDelegate(self) - self.setItemDelegateForColumn(0, delegate1) + # self.clicked.connect(self.on_clicked) + # delegate1 = ClientSubmissionDelegate(self) + # self.setItemDelegateForColumn(0, delegate1) self.model = model self.setModel(self.model) # self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents) @@ -276,14 +276,42 @@ class SubmissionsTree(QTreeView): self.doubleClicked.connect(self.show_details) # self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # self.customContextMenuRequested.connect(self.open_menu) + self.setStyleSheet(""" + QTreeView { + background-color: #f5f5f5; + alternate-background-color: "#cfe2f3"; + border: 1px solid #d3d3d3; + } + QTreeView::item { + padding: 5px; + border-bottom: 1px solid #d3d3d3; + } + QTreeView::item:selected { + background-color: #0078d7; + color: white; + } + """) + + # Enable alternating row colors + self.setAlternatingRowColors(True) + self.setIndentation(20) + self.setItemsExpandable(True) + self.expanded.connect(self.expand_item) for ii in range(2): self.resizeColumnToContents(ii) - @pyqtSlot(QModelIndex) - def on_clicked(self, index): - if not index.parent().isValid() and index.column() == 0: - self.setExpanded(index, not self.isExpanded(index)) + # @pyqtSlot(QModelIndex) + # def on_clicked(self, index): + # if not index.parent().isValid() and index.column() == 0: + # self.setExpanded(index, not self.isExpanded(index)) + + def expand_item(self, event: QModelIndex): + logger.debug(f"Data: {event.data()}") + logger.debug(f"Parent {event.parent().data()}") + logger.debug(f"Row: {event.row()}") + logger.debug(f"Sibling: {event.siblingAtRow(event.row()).data()}") + logger.debug(f"Model: {event.model().event()}") def contextMenuEvent(self, event: QContextMenuEvent): """ @@ -306,19 +334,30 @@ class SubmissionsTree(QTreeView): self.menu = QMenu(self) self.con_actions = query_obj.custom_context_events for key in self.con_actions.keys(): - if key.lower() == "add procedure": - action = QMenu(self.menu) - action.setTitle("Add Procedure") - for procedure in query_obj.allowed_procedures: - proc_name = procedure.name - proc = QAction(proc_name, action) - proc.triggered.connect(lambda _, procedure_name=proc_name: self.con_actions['Add Procedure'](obj=self, proceduretype_name=procedure_name)) - action.addAction(proc) - self.menu.addMenu(action) - else: - action = QAction(key, self) - action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self)) - self.menu.addAction(action) + logger.debug(key) + match key.lower(): + case "add procedure": + action = QMenu(self.menu) + action.setTitle("Add Procedure") + for procedure in query_obj.allowed_procedures: + proc_name = procedure.name + proc = QAction(proc_name, action) + proc.triggered.connect(lambda _, procedure_name=proc_name: self.con_actions['Add Procedure'](obj=self, proceduretype_name=procedure_name)) + action.addAction(proc) + self.menu.addMenu(action) + case "add results": + action = QMenu(self.menu) + action.setTitle("Add Results") + for results in query_obj.proceduretype.allowed_result_methods: + res_name = results + res = QAction(res_name, action) + res.triggered.connect(lambda _, procedure_name=res_name: self.con_actions['Add Results'](obj=self, resultstype_name=procedure_name)) + action.addAction(res) + self.menu.addMenu(action) + case _: + action = QAction(key, self) + action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self)) + self.menu.addAction(action) # # NOTE: add other required actions self.menu.popup(QCursor.pos()) @@ -334,36 +373,27 @@ class SubmissionsTree(QTreeView): root = self.model.invisibleRootItem() for submission in self.data: group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}" - submission_item = self.model.add_group(parent=root, item_data=dict( + submission_item = self.model.add_child(parent=root, child=dict( name=group_str, query_str=submission['submitter_plate_id'], item_type=ClientSubmission )) + logger.debug(f"Added {submission_item}") for run in submission['run']: # self.model.append_element_to_group(group_item=group_item, element=run) - run_item = self.model.add_group(parent=submission_item, item_data=dict( + run_item = self.model.add_child(parent=submission_item, child=dict( name=run['plate_number'], query_str=run['plate_number'], item_type=Run )) - + logger.debug(f"Added {run_item}") for procedure in run['procedures']: - self.model.add_group(parent=run_item, item_data=dict( + procedure_item = self.model.add_child(parent=run_item, child=dict( name=procedure['name'], query_str=procedure['name'], item_type=Procedure )) - # root = self.model.invisibleRootItem() - # for submission in self.data: - # submission_item = QStandardItem(submission['name']) - # root.appendRow(submission_item) - # for run in submission['run']: - # run_item = QStandardItem(run['name']) - # submission_item.appendRow(run_item) - # for procedure in run['procedures']: - # procedure_item = QStandardItem(procedure['name']) - # run_item.appendRow(procedure_item) - # self._populateTree(self.data, self.model.invisibleRootItem()) + logger.debug(f"Added {procedure_item}") def _populateTree(self, children, parent): for child in children: @@ -381,14 +411,20 @@ class SubmissionsTree(QTreeView): self.model.setRowCount(0) # works def show_details(self, sel: QModelIndex): - id = self.selectionModel().currentIndex() + # 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 - Run.query(id=id).show_details(self) + # id = id.sibling(id.row(), 1) + indexes = self.selectedIndexes() + + dicto = next((item.data(1) for item in indexes if item.data(1))) + logger.debug(dicto) + # try: + # id = int(id.data()) + # except ValueError: + # return + # Run.query(id=id).show_details(self) + obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) + logger.debug(obj) def link_extractions(self): pass @@ -402,59 +438,18 @@ class ClientSubmissionRunModel(QStandardItemModel): def __init__(self, parent=None): super(ClientSubmissionRunModel, self).__init__(parent) - headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"] - self.setColumnCount(len(headers)) - self.setHorizontalHeaderLabels(headers) + # headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"] + # self.setColumnCount(len(headers)) + # self.setHorizontalHeaderLabels(headers) - def add_group(self, parent, item_data): - item_root = QStandardItem() - item_root.setEditable(False) - item = QStandardItem(item_data['name']) + + + def add_child(self, parent: QStandardItem, child:dict): + item = QStandardItem(child['name']) + item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1) + parent.appendRow(item) item.setEditable(False) - i = parent.rowCount() - for j, it in enumerate((item_root, item)): - # NOTE: Adding item to invisible root row i, column j (wherever j comes from) - parent.setChild(i, j, it) - parent.setEditable(False) - for j in range(self.columnCount()): - it = parent.child(i, j) - if it is None: - # NOTE: Set invisible root child to empty if it is None. - it = QStandardItem() - parent.setChild(i, j, it) - item_root.setData(dict(item_type=item_data['item_type'], query_str=item_data['query_str']), 1) - return item_root - - def append_element_to_group(self, group_item, item_data: dict): - # logger.debug(f"Element: {pformat(element)}") - j = group_item.rowCount() - item_icon = QStandardItem() - item_icon.setEditable(False) - # item_icon.setBackground(QColor("#0D1225")) - # item_icon.setData(dict(item_type="Run", query_str=element['plate_number']), 1) - # 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: - key = None - if not key: - continue - value = str(item_data[key]) - item = QStandardItem(value) - item.setBackground(QColor("#CFE2F3")) - item.setEditable(False) - # item_icon.setChild(j, i, item) - item.setData(dict(item_type=item_data['item_type'], query_str=item_data['query_str']),1) - group_item.setChild(j, i, item) - # group_item.setChild(j, 1, QStandardItem("B")) return item - def get_value(self, idx: int, column: int = 1): - return self.item(idx, column) - - def query_group_object(self, idx: int): - row_obj = self.get_value(idx) - logger.debug(row_obj.query_str) - return self.sql_object.query(name=row_obj.query_str, limit=1) + def edit_item(self): + pass \ No newline at end of file diff --git a/src/submissions/templates/equipment_details.html b/src/submissions/templates/equipment_details.html index a2ab184..34906c3 100644 --- a/src/submissions/templates/equipment_details.html +++ b/src/submissions/templates/equipment_details.html @@ -23,10 +23,10 @@ {% endif %} {% endblock %} - {% endblock %} - + \ No newline at end of file diff --git a/src/submissions/templates/process_details.html b/src/submissions/templates/process_details.html index c76c587..950cafb 100644 --- a/src/submissions/templates/process_details.html +++ b/src/submissions/templates/process_details.html @@ -23,10 +23,10 @@ {% endif %} {% endblock %} - {% endblock %} - + \ No newline at end of file diff --git a/src/submissions/templates/reagent_details.html b/src/submissions/templates/reagent_details.html index 95ddaa4..3e57373 100644 --- a/src/submissions/templates/reagent_details.html +++ b/src/submissions/templates/reagent_details.html @@ -23,9 +23,10 @@ {% endif %} {% endblock %} - {% endblock %} - + \ No newline at end of file diff --git a/src/submissions/templates/run_details.html b/src/submissions/templates/run_details.html index 2df1fe4..b9e15b0 100644 --- a/src/submissions/templates/run_details.html +++ b/src/submissions/templates/run_details.html @@ -77,10 +77,10 @@

- {% endblock %} - + diff --git a/src/submissions/templates/sample_checker.html b/src/submissions/templates/sample_checker.html index 821f52d..a2c7146 100644 --- a/src/submissions/templates/sample_checker.html +++ b/src/submissions/templates/sample_checker.html @@ -30,9 +30,10 @@ {% endblock %} - {% endblock %} - \ No newline at end of file diff --git a/src/submissions/templates/sample_details.html b/src/submissions/templates/sample_details.html index 5fc8571..1390dde 100644 --- a/src/submissions/templates/sample_details.html +++ b/src/submissions/templates/sample_details.html @@ -21,9 +21,10 @@ {% endif %} {% endblock %} - {% endblock %} - + diff --git a/src/submissions/templates/tips_details.html b/src/submissions/templates/tips_details.html index 65cb588..0d257a8 100644 --- a/src/submissions/templates/tips_details.html +++ b/src/submissions/templates/tips_details.html @@ -23,10 +23,10 @@ {% endif %} {% endblock %} - {% endblock %} - + \ No newline at end of file