diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index e496e6f..07a2ae1 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1320,11 +1320,20 @@ class Procedure(BaseClass): Returns: dict: dictionary of functions """ - names = ["Add Results", "Edit", "Add Comment", "Show Details", "Delete"] + names = ["Add Results", "Add Equipment", "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}") + from frontend.widgets import results + results_class = getattr(results, resultstype_name) + rs = results_class(procedure=self, parent=obj) + + def add_equipment(self, obj): + from frontend.widgets.equipment_usage import EquipmentUsage + dlg = EquipmentUsage(parent=obj, procedure=self) + if dlg.exec(): + pass def edit(self, obj): logger.debug("Edit!") @@ -2668,7 +2677,6 @@ class Results(BaseClass): 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)) diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 81573b0..d0069a7 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -314,6 +314,7 @@ class ClientSubmission(BaseClass, LogMixin): assoc.save() else: logger.warning("Run cancelled.") + obj.set_data() def edit(self, obj): @@ -1138,8 +1139,7 @@ class Run(BaseClass, LogMixin): sql, _ = dlg.return_sql() logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}") sql.save() - - + obj.set_data() def delete(self, obj=None): """ @@ -2029,7 +2029,7 @@ class RunSampleAssociation(BaseClass): class ProcedureSampleAssociation(BaseClass): - id = Column(INTEGER, primary_key=True) + id = Column(INTEGER, unique=True, nullable=False) 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) @@ -2042,15 +2042,34 @@ class ProcedureSampleAssociation(BaseClass): results = relationship("Results", back_populates="sampleprocedureassociation") + @classmethod + def query(cls, sample: Sample|str|None=None, procedure: Procedure|str|None=None, limit: int=0, **kwargs): + query = cls.__database_session__.query(cls) + match sample: + case Sample(): + query = query.filter(cls.sample==sample) + case str(): + query = query.join(Sample).filter(Sample.sample_id==sample) + case _: + pass + match procedure: + case Procedure(): + query = query.filter(cls.procedure == procedure) + case str(): + query = query.join(Procedure).filter(Procedure.name == procedure) + case _: + pass + if sample and procedure: + limit = 1 + return cls.execute_query(query=query, limit=limit, **kwargs) - # 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) + def __init__(self, new_id:int|None=None, **kwarg): + if new_id: + self.id = new_id + else: + self.id = self.__class__.autoincrement_id() + super().__init__(**kwarg) @classmethod diff --git a/src/submissions/backend/excel/parsers/__init__.py b/src/submissions/backend/excel/parsers/__init__.py index 6263e1d..277572f 100644 --- a/src/submissions/backend/excel/parsers/__init__.py +++ b/src/submissions/backend/excel/parsers/__init__.py @@ -7,6 +7,7 @@ from typing import Generator, Tuple from openpyxl import load_workbook from pandas import DataFrame from backend.validators import pydant +from backend.db.models import Procedure logger = logging.getLogger(f"submissions.{__name__}") @@ -15,7 +16,8 @@ class DefaultParser(object): def __repr__(self): return f"{self.__class__.__name__}<{self.filepath.stem}>" - def __init__(self, filepath: Path | str, range_dict: dict | None = None, *args, **kwargs): + def __init__(self, filepath: Path | str, procedure: Procedure|None=None, range_dict: dict | None = None, *args, **kwargs): + self.procedure = procedure try: self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}") except AttributeError: diff --git a/src/submissions/backend/excel/parsers/pcr_parser.py b/src/submissions/backend/excel/parsers/pcr_parser.py index 427aaf5..05a832f 100644 --- a/src/submissions/backend/excel/parsers/pcr_parser.py +++ b/src/submissions/backend/excel/parsers/pcr_parser.py @@ -8,44 +8,14 @@ from typing import Generator, Tuple from openpyxl import load_workbook -from backend.db.models import Run, Sample +from backend.db.models import Run, Sample, Procedure, ProcedureSampleAssociation 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, @@ -72,11 +42,10 @@ class PCRInfoParser(DefaultKEYVALUEParser): # def to_pydantic(self): - from backend.db.models import Procedure + # 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) - + return self._pyd_object(**data, parent=self.procedure) # @property # def pcr_info(self) -> dict: @@ -97,3 +66,36 @@ class PCRInfoParser(DefaultKEYVALUEParser): # pcr[key] = value # pcr['imported_by'] = getuser() # return pcr + + +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" and k != "sample"} + yield {sample: multi} + + def to_pydantic(self): + logger.debug(f"running to pydantic") + for item in self.parsed_info: + sample_obj = Sample.query(sample_id=list(item.keys())[0]) + logger.debug(f"Sample object {sample_obj}") + assoc = ProcedureSampleAssociation.query(sample=sample_obj, procedure=self.procedure) + if assoc and not isinstance(assoc, list): + yield self._pyd_object(results=list(item.values())[0], parent=assoc) + else: + continue + diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 11cbb1f..4a4e844 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -1436,9 +1436,15 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): sql.run = self.run if self.proceduretype: sql.proceduretype = self.proceduretype - for sample in self.samples: - if sample.sample_id.startswith("blank_") or sample.sample_id == "": - continue + try: + start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 + except ValueError: + start_index = 1 + relevant_samples = [sample for sample in self.samples if not sample.sample_id.startswith("blank_") and not sample.sample_id == ""] + logger.debug(f"start index: {start_index}") + assoc_id_range = range(start_index, start_index + len(relevant_samples)+1) + logger.debug(f"Association id range: {assoc_id_range}") + for iii, sample in enumerate(relevant_samples): sample_sql = sample.to_sql() if sql.run: if sample_sql not in sql.run.sample: @@ -1446,7 +1452,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): run_assoc = RunSampleAssociation(sample=sample_sql, run=self.run, row=sample.row, column=sample.column) else: logger.debug(f"sample {sample_sql} found in {sql.run.sample}") - proc_assoc = ProcedureSampleAssociation(procedure=sql, sample=sample_sql, row=sample.row, column=sample.column) + proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql, row=sample.row, column=sample.column) if self.kittype['value'] not in ["NA", None, ""]: kittype = KitType.query(name=self.kittype['value'], limit=1) if kittype: @@ -1535,5 +1541,17 @@ class PydClientSubmission(PydBaseClass): class PydResults(PydBaseClass, arbitrary_types_allowed=True): results: dict = Field(default={}) - parent: Sample|Procedure + img: None = Field(default=None) + parent: Procedure|ProcedureSampleAssociation|None = Field(default=None) + + def to_sql(self): + sql = Results(result=self.results) + match self.parent: + case ProcedureSampleAssociation(): + sql.sampleprocedureassociation = self.parent + case Procedure(): + sql.procedure = self.parent + case _: + logger.error("Improper association found.") + return sql diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index f0da363..4ea635d 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -19,7 +19,7 @@ class EquipmentUsage(QDialog): def __init__(self, parent, procedure: Procedure): super().__init__(parent) self.procedure = procedure - self.setWindowTitle(f"Equipment Checklist - {procedure.rsl_plate_num}") + self.setWindowTitle(f"Equipment Checklist - {procedure.name}") self.used_equipment = self.procedure.equipment self.kit = self.procedure.kittype self.opt_equipment = procedure.proceduretype.get_equipment() diff --git a/src/submissions/frontend/widgets/functions.py b/src/submissions/frontend/widgets/functions.py index bf33e39..cb97621 100644 --- a/src/submissions/frontend/widgets/functions.py +++ b/src/submissions/frontend/widgets/functions.py @@ -39,6 +39,7 @@ def select_open_file(obj: QMainWindow, file_extension: str | None = None) -> Pat logger.warning(f"No file selected, cancelling.") return obj.last_dir = fname.parent + logger.debug(f"File selected: {fname}") return fname diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index e1454fb..c9c68aa 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -70,8 +70,8 @@ class ProcedureCreation(QDialog): procedure=self.created_procedure.__dict__, plate_map=self.plate_map ) - with open("procedure.html", "w") as f: - f.write(html) + # with open("procedure.html", "w") as f: + # f.write(html) self.webview.setHtml(html) @pyqtSlot(str, str) diff --git a/src/submissions/frontend/widgets/results/PCR.py b/src/submissions/frontend/widgets/results/PCR.py deleted file mode 100644 index 50fbe03..0000000 --- a/src/submissions/frontend/widgets/results/PCR.py +++ /dev/null @@ -1,26 +0,0 @@ -""" - -""" -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 index 12dc63a..3e64eda 100644 --- a/src/submissions/frontend/widgets/results/__init__.py +++ b/src/submissions/frontend/widgets/results/__init__.py @@ -4,4 +4,4 @@ class DefaultResults(object): pass -from .PCR import pcr \ No newline at end of file +from .pcr import PCR \ No newline at end of file diff --git a/src/submissions/frontend/widgets/results/pcr.py b/src/submissions/frontend/widgets/results/pcr.py new file mode 100644 index 0000000..c720260 --- /dev/null +++ b/src/submissions/frontend/widgets/results/pcr.py @@ -0,0 +1,39 @@ +""" + +""" +import logging +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 tools import get_application_from_parent +from . import DefaultResults + +logger = logging.getLogger(f"submissions.{__name__}") + +class PCR(DefaultResults): + + def __init__(self, procedure: Procedure, parent, fname:Path|str|None=None): + logger.debug(f"FName before correction: {fname}") + self.procedure = procedure + if not fname: + self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent)) + elif isinstance(fname, str): + self.fname = Path(fname) + logger.debug(f"FName after correction: {fname}") + self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure) + self.sample_parser = PCRSampleParser(filepath=self.fname, procedure=self.procedure) + self.build_procedure() + self.build_samples() + + def build_procedure(self): + procedure_info = self.info_parser.to_pydantic() + procedure_sql = procedure_info.to_sql() + procedure_sql.save() + + def build_samples(self): + samples = self.sample_parser.to_pydantic() + for sample in samples: + sql = sample.to_sql() + sql.save() diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 3c69558..a450154 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -321,14 +321,11 @@ class SubmissionsTree(QTreeView): event (_type_): the item of interest """ indexes = self.selectedIndexes() - dicto = next((item.data(1) for item in indexes if item.data(1))) query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) logger.debug(query_obj) - # NOTE: Convert to data in id column (i.e. column 0) # id = id.sibling(id.row(), 0).data() - # logger.debug(id.model().query_group_object(id.row())) # clientsubmission = id.model().query_group_object(id.row()) self.menu = QMenu(self) @@ -403,8 +400,6 @@ class SubmissionsTree(QTreeView): if isinstance(children, List): self._populateTree(child, child_item) - - def clear(self): if self.model != None: # self.model.clear() # works @@ -415,7 +410,6 @@ class SubmissionsTree(QTreeView): # NOTE: Convert to data in id column (i.e. column 0) # 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: