From 87efb185187d75af15d8e47d28a814f15e01be77 Mon Sep 17 00:00:00 2001 From: lwark Date: Thu, 14 Aug 2025 09:52:01 -0500 Subject: [PATCH] Moments before disaster --- src/submissions/backend/db/models/__init__.py | 29 +- .../backend/db/models/procedures.py | 730 ++++++++++-------- .../backend/db/models/submissions.py | 113 ++- src/submissions/backend/validators/pydant.py | 98 +-- .../frontend/widgets/procedure_creation.py | 2 +- .../frontend/widgets/submission_details.py | 2 +- 6 files changed, 519 insertions(+), 455 deletions(-) diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 5084f30..9da0905 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -2,11 +2,7 @@ Contains all models for sqlalchemy """ from __future__ import annotations - import sys, logging, json -from collections import OrderedDict - -import sqlalchemy.exc from dateutil.parser import parse from pandas import DataFrame from pydantic import BaseModel @@ -21,7 +17,6 @@ from pathlib import Path from sqlalchemy.orm.relationships import _RelationshipDeclared from tools import report_result, list_sort_dict - # NOTE: Load testing environment if 'pytest' in sys.modules: sys.path.append(Path(__file__).parents[4].absolute().joinpath("tests").__str__()) @@ -41,9 +36,9 @@ class BaseClass(Base): __table_args__ = {'extend_existing': True} #: NOTE Will only add new columns singles = ['id'] - omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"] - omni_sort = ["name"] - omni_inheritable = [] + # omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"] + # omni_sort = ["name"] + # omni_inheritable = [] searchables = [] _misc_info = Column(JSON) @@ -242,12 +237,13 @@ class BaseClass(Base): @classmethod def query_or_create(cls, **kwargs) -> Tuple[Any, bool]: new = False - allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)] + allowed = [k for k, v in cls.__dict__.items() if + isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)] # and not isinstance(v.property, _RelationshipDeclared)] sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed} outside_kwargs = {k: v for k, v in kwargs.items() if k not in allowed} - # logger.debug(f"Sanitized kwargs: {sanitized_kwargs}") - instance = cls.query(**sanitized_kwargs) + logger.debug(f"Sanitized kwargs: {sanitized_kwargs}") + instance = cls.query(limit=1, **sanitized_kwargs) if not instance or isinstance(instance, list): instance = cls() new = True @@ -280,7 +276,8 @@ class BaseClass(Base): return cls.execute_query(**kwargs) @classmethod - def execute_query(cls, query: Query = None, model=None, limit: int = 0, offset:int|None=None, **kwargs) -> Any | List[Any]: + def execute_query(cls, query: Query = None, model=None, limit: int = 0, offset: int | None = None, + **kwargs) -> Any | List[Any]: """ Execute sqlalchemy query with relevant defaults. @@ -610,12 +607,12 @@ class BaseClass(Base): output_date = datetime.combine(output_date, addition_time).strftime("%Y-%m-%d %H:%M:%S") return output_date - def details_dict(self, **kwargs): + def details_dict(self, **kwargs) -> dict: relevant = {k: v for k, v in self.__class__.__dict__.items() if isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)} # output = OrderedDict() - output = dict(excluded = ["excluded", "misc_info", "_misc_info", "id"]) + output = dict(excluded=["excluded", "misc_info", "_misc_info", "id"]) for k, v in relevant.items(): try: check = v.foreign_keys @@ -666,7 +663,7 @@ class BaseClass(Base): output[k] = value return output - def to_pydantic(self, pyd_model_name:str|None=None, **kwargs): + def to_pydantic(self, pyd_model_name: str | None = None, **kwargs): from backend.validators import pydant if not pyd_model_name: pyd_model_name = f"Pyd{self.__class__.__name__}" @@ -685,7 +682,7 @@ class BaseClass(Base): if dlg.exec(): pass - def export(self, obj, output_filepath: str|Path|None=None): + def export(self, obj, output_filepath: str | Path | None = None): # if not hasattr(self, "template_file"): # logger.error(f"Export not implemented for {self.__class__.__name__}") # return diff --git a/src/submissions/backend/db/models/procedures.py b/src/submissions/backend/db/models/procedures.py index 4971074..c2aa348 100644 --- a/src/submissions/backend/db/models/procedures.py +++ b/src/submissions/backend/db/models/procedures.py @@ -44,13 +44,13 @@ equipmentrole_equipment = Table( extend_existing=True ) -equipment_process = Table( - "_equipment_process", - Base.metadata, - Column("process_id", INTEGER, ForeignKey("_process.id")), - Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), - extend_existing=True -) +# equipment_process = Table( +# "_equipment_process", +# Base.metadata, +# Column("process_id", INTEGER, ForeignKey("_process.id")), +# Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), +# extend_existing=True +# ) equipmentrole_process = Table( "_equipmentrole_process", @@ -68,15 +68,15 @@ equipmentrole_process = Table( # extend_existing=True # ) -tiprole_tips = Table( - "_tiprole_tips", - Base.metadata, - Column("tiprole_id", INTEGER, ForeignKey("_tiprole.id")), - Column("tips_id", INTEGER, ForeignKey("_tips.id")), - extend_existing=True -) +# tiprole_tips = Table( +# "_tiprole_tips", +# Base.metadata, +# Column("tiprole_id", INTEGER, ForeignKey("_tiprole.id")), +# Column("tips_id", INTEGER, ForeignKey("_tips.id")), +# extend_existing=True +# ) -process_tiprole = Table( +process_tips = Table( "_process_tiprole", Base.metadata, Column("process_id", INTEGER, ForeignKey("_process.id")), @@ -84,13 +84,13 @@ process_tiprole = Table( extend_existing=True ) -equipment_tips = Table( - "_equipment_tips", - Base.metadata, - Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), - Column("tips_id", INTEGER, ForeignKey("_tips.id")), - extend_existing=True -) +# equipment_tips = Table( +# "_equipment_tips", +# Base.metadata, +# Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), +# Column("tips_id", INTEGER, ForeignKey("_tips.id")), +# extend_existing=True +# ) # kittype_procedure = Table( # "_kittype_procedure", @@ -100,13 +100,13 @@ equipment_tips = Table( # extend_existing=True # ) -proceduretype_process = Table( - "_proceduretype_process", - Base.metadata, - Column("process_id", INTEGER, ForeignKey("_process.id")), - Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")), - extend_existing=True -) +# proceduretype_process = Table( +# "_proceduretype_process", +# Base.metadata, +# Column("process_id", INTEGER, ForeignKey("_process.id")), +# Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")), +# extend_existing=True +# ) submissiontype_proceduretype = Table( "_submissiontype_proceduretype", @@ -823,7 +823,7 @@ class Reagent(BaseClass, LogMixin): class ReagentLot(BaseClass): id = Column(INTEGER, primary_key=True) #: primary key - lot = Column(String(64)) #: lot number of reagent + lot = Column(String(64), unique=True) #: lot number of reagent expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically reagent = relationship("Reagent") #: joined parent reagent type reagent_id = Column(INTEGER, ForeignKey("_reagent.id", ondelete='SET NULL', @@ -843,6 +843,25 @@ class ReagentLot(BaseClass): def name(self): return self.lot + @classmethod + def query(cls, + lot: str | None = None, + name: str | None = None, + limit: int = 1, + **kwargs) -> ReagentLot | List[ReagentLot]: + query: Query = cls.__database_session__.query(cls) + match lot: + case str(): + query = query.filter(cls.lot == lot) + case _: + pass + match name: + case str(): + query = query.join(Reagent).filter(Reagent.name == name) + case _: + pass + return cls.execute_query(query=query, limit=limit) + def __repr__(self): return f"" @@ -1447,7 +1466,7 @@ class Procedure(BaseClass): ) #: Relation to ProcedureReagentAssociation reagentlot = association_proxy("procedurereagentlotassociation", - "reagentlot", creator=lambda reg: ProcedureReagentLotAssociation( + "reagentlot", creator=lambda reg: ProcedureReagentLotAssociation( reagent=reg)) #: Association proxy to RunReagentAssociation.reagent procedureequipmentassociation = relationship( @@ -1459,13 +1478,13 @@ class Procedure(BaseClass): equipment = association_proxy("procedureequipmentassociation", "equipment") #: Association proxy to RunEquipmentAssociation.equipment - proceduretipsassociation = relationship( - "ProcedureTipsAssociation", - back_populates="procedure", - cascade="all, delete-orphan") - - tips = association_proxy("proceduretipsassociation", - "tips") + # proceduretipsassociation = relationship( + # "ProcedureTipsAssociation", + # back_populates="procedure", + # cascade="all, delete-orphan") + # + # tips = association_proxy("proceduretipsassociation", + # "tips") @validates('repeat') def validate_repeat(self, key, value): @@ -1477,7 +1496,8 @@ class Procedure(BaseClass): @classmethod @setup_lookup - def query(cls, id: int | None = None, name: str | None = None, start_date: date | datetime | str | int | None = None, + def query(cls, id: int | None = None, name: str | None = None, + start_date: date | datetime | str | int | None = None, end_date: date | datetime | str | int | None = None, limit: int = 0, **kwargs) -> Procedure | List[ Procedure]: query: Query = cls.__database_session__.query(cls) @@ -1610,10 +1630,7 @@ class Procedure(BaseClass): output['tips'] = [tips.details_dict() for tips in output['proceduretipsassociation']] output['repeat'] = bool(output['repeat']) output['run'] = self.run.name - output['excluded'] += ['id', "results", "proceduresampleassociation", "sample", - "procedurereagentlotassociation", - "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", - "tips", "control", "kittype"] + output['excluded'] += self.get_default_info("details_ignore") output['sample_count'] = len(active_samples) output['clientlab'] = self.run.clientsubmission.clientlab.name output['cost'] = 0.00 @@ -1670,11 +1687,17 @@ class Procedure(BaseClass): def get_default_info(cls, *args) -> dict | list | str: dicto = super().get_default_info() recover = ['filepath', 'sample', 'csv', 'comment', 'equipment'] + # ['id', "results", "proceduresampleassociation", "sample", + # "procedurereagentlotassociation", + # "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", + # "tips", "control", "kittype"] dicto.update(dict( - details_ignore=['excluded', 'reagents', 'sample', - 'extraction_info', 'comment', 'barcode', - 'platemap', 'export_map', 'equipment', 'tips', 'custom', 'reagentlot', - 'procedurereagentassociation'], + details_ignore=['excluded', 'reagents', 'sample', 'extraction_info', 'comment', 'barcode', + 'platemap', 'export_map', 'equipment', 'tips', 'custom', 'reagentlot', 'reagent_lot', + "results", "proceduresampleassociation", "sample", + "procedurereagentlotassociation", + "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", + "tips", "control"], # NOTE: Fields not placed in ui form form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer', 'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre', 'completed_date', @@ -1683,11 +1706,15 @@ class Procedure(BaseClass): form_recover=recover )) if args: - output = {k: v for k, v in dicto.items() if k in args} + if len(args) > 1: + output = {k: v for k, v in dicto.items() if k in args} + else: + output = dicto[args[0]] else: output = {k: v for k, v in dicto.items()} return output + # class ProcedureTypeKitTypeAssociation(BaseClass): # """ # Abstract of relationship between kits and their procedure type. @@ -1824,9 +1851,9 @@ class ProcedureTypeReagentRoleAssociation(BaseClass): DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html """ - omni_removes = BaseClass.omni_removes + ["submission_type_id", "kits_id", "reagent_roles_id", "last_used"] - omni_sort = ["proceduretype", "kittype", "reagentrole", "required", "uses"] - omni_inheritable = ["proceduretype", "kittype"] + # omni_removes = BaseClass.omni_removes + ["submission_type_id", "kits_id", "reagent_roles_id", "last_used"] + # omni_sort = ["proceduretype", "kittype", "reagentrole", "required", "uses"] + # omni_inheritable = ["proceduretype", "kittype"] reagentrole_id = Column(INTEGER, ForeignKey("_reagentrole.id"), primary_key=True) #: id of associated reagent type @@ -2076,7 +2103,7 @@ class ProcedureReagentLotAssociation(BaseClass): str: Representation of this RunReagentAssociation """ try: - return f"" + return f"" except AttributeError: try: logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!") @@ -2584,189 +2611,12 @@ class EquipmentRole(BaseClass): return output -class ProcedureEquipmentAssociation(BaseClass): - """ - Abstract association between BasicRun and Equipment - """ - - equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment - procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure - equipmentrole = Column(String(64), primary_key=True) #: name of the reagentrole the equipment fills - processversion_id = Column(INTEGER, ForeignKey("_processversion.id", ondelete="SET NULL", - name="SEA_Process_id")) #: Foreign key of process id - start_time = Column(TIMESTAMP) #: start time of equipment use - end_time = Column(TIMESTAMP) #: end time of equipment use - comments = Column(String(1024)) #: comments about equipment - - procedure = relationship(Procedure, - back_populates="procedureequipmentassociation") #: associated procedure - - equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment - - tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete="SET NULL", - name="SEA_Process_id")) - - def __repr__(self) -> str: - try: - return f"" - except AttributeError: - return "" - - def __init__(self, procedure=None, equipment=None, procedure_id: int | None = None, equipment_id: int | None = None, - equipmentrole: str = "None"): - if not procedure: - if procedure_id: - procedure = Procedure.query(id=procedure_id) - else: - logger.error("Creation error") - self.procedure = procedure - if not equipment: - if equipment_id: - equipment = Equipment.query(id=equipment_id) - else: - logger.error("Creation error") - self.equipment = equipment - if isinstance(equipmentrole, list): - equipmentrole = equipmentrole[0] - if isinstance(equipmentrole, EquipmentRole): - equipmentrole = equipmentrole.name - self.equipmentrole = equipmentrole - - @property - def name(self): - return f"{self.procedure.name} & {self.equipment.name}" - - @property - def process(self): - return ProcessVersion.query(id=self.processversion_id) - - @property - def tips(self): - try: - return Tips.query(id=self.tips_id, limit=1) - except AttributeError: - return None - - def to_sub_dict(self) -> dict: - """ - This RunEquipmentAssociation as a dictionary - - Returns: - dict: This RunEquipmentAssociation as a dictionary - """ - try: - process = self.process.name - except AttributeError: - process = "No process found" - output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, - processes=[process], role=self.equipmentrole, nickname=self.equipment.nickname) - return output - - def to_pydantic(self) -> "PydEquipment": - """ - Returns a pydantic model based on this object. - - Returns: - PydEquipment: pydantic equipment model - """ - from backend.validators import PydEquipment - return PydEquipment(**self.details_dict()) - - @classmethod - @setup_lookup - def query(cls, - equipment: int | Equipment | None = None, - procedure: int | Procedure | None = None, - equipmentrole: str | None = None, - limit: int = 0, **kwargs) \ - -> Any | List[Any]: - query: Query = cls.__database_session__.query(cls) - match equipment: - case int(): - query = query.filter(cls.equipment_id == equipment) - case Equipment(): - query = query.filter(cls.equipment == equipment) - case _: - pass - match procedure: - case int(): - query = query.filter(cls.procedure_id == procedure) - case Procedure(): - query = query.filter(cls.procedure == procedure) - case _: - pass - if equipmentrole is not None: - query = query.filter(cls.equipmentrole == equipmentrole) - return cls.execute_query(query=query, limit=limit, **kwargs) - - def details_dict(self, **kwargs): - output = super().details_dict() - # NOTE: Figure out how to merge the misc_info if doing .update instead. - relevant = {k: v for k, v in output.items() if k not in ['equipment']} - output = output['equipment'].details_dict() - misc = output['misc_info'] - output.update(relevant) - output['misc_info'] = misc - output['equipment_role'] = self.equipmentrole - output['process'] = self.process.details_dict() - try: - output['tips'] = self.tips.details_dict() - except AttributeError: - output['tips'] = None - return output - - -class ProcedureTypeEquipmentRoleAssociation(BaseClass): - """ - Abstract association between SubmissionType and EquipmentRole - """ - equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment - proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), - primary_key=True) #: id of associated procedure - # kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), - # primary_key=True) - uses = Column(JSON) #: locations of equipment on the procedure type excel sheet. - static = Column(INTEGER, - default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list? - - proceduretype = relationship(ProcedureType, - back_populates="proceduretypeequipmentroleassociation") #: associated procedure - - equipmentrole = relationship(EquipmentRole, - back_populates="equipmentroleproceduretypeassociation") #: associated equipment - - # kittype = relationship(KitType, back_populates="proceduretypeequipmentroleassociation") - - @validates('static') - def validate_static(self, key, value): - """ - Ensures only 1 & 0 used in 'static' - - Args: - key (str): name of attribute - value (_type_): value of attribute - - Raises: - ValueError: Raised if bad value given - - Returns: - _type_: value - """ - if not 0 <= value < 2: - raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') - return value - - @check_authorization - def save(self): - super().save() - - class Process(BaseClass): """ A Process is a method used by a piece of equipment. """ - level = 2 + id = Column(INTEGER, primary_key=True) #: Process id, primary key name = Column(String(64), unique=True) #: Process name @@ -2885,20 +2735,20 @@ class Process(BaseClass): def details_dict(self, **kwargs): output = super().details_dict(**kwargs) output['processversion'] = [item.details_dict() for item in self.processversion] - logger.debug(f"Process output dict: {pformat(output)}") + # logger.debug(f"Process output dict: {pformat(output)}") return output - def to_pydantic(self): - from backend.validators.pydant import PydProcess - output = {} - for k, v in self.details_dict().items(): - if isinstance(v, list): - output[k] = [item.name if issubclass(item.__class__, BaseClass) else item for item in v] - elif issubclass(v.__class__, BaseClass): - output[k] = v.name - else: - output[k] = v - return PydProcess(**output) + # def to_pydantic(self): + # from backend.validators.pydant import PydProcess + # output = {} + # for k, v in self.details_dict().items(): + # if isinstance(v, list): + # output[k] = [item.name if issubclass(item.__class__, BaseClass) else item for item in v] + # elif issubclass(v.__class__, BaseClass): + # output[k] = v.name + # else: + # output[k] = v + # return PydProcess(**output) class ProcessVersion(BaseClass): @@ -2923,26 +2773,51 @@ class ProcessVersion(BaseClass): output['project'] = "" return output + def set_attribute(self, key, value): + setattr(self, key, value) -class TipRole(BaseClass): + @classmethod + def query(cls, + version: str | None = None, + name: str | None = None, + limit: int = 0, + **kwargs) -> ReagentLot | List[ReagentLot]: + query: Query = cls.__database_session__.query(cls) + match name: + case str(): + query = query.join(Process).filter(Process.name == name) + case _: + pass + match version: + case str(): + query = query.filter(cls.version == version) + case _: + pass + return cls.execute_query(query=query, limit=limit) + + +class Tips(BaseClass): """ An abstract reagentrole that a tip fills during a process """ id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(64)) #: name of reagent type - tips = relationship("Tips", back_populates="tiprole", + tipslot = relationship("TipsLot", back_populates="tips", secondary=tiprole_tips) #: concrete control of this reagent type - process = relationship("Process", back_populates="tiprole", secondary=process_tiprole) + manufacturer = Column(String(64)) + capacity = Column(INTEGER) + ref = Column(String(64)) #: tip reference number + process = relationship("Process", back_populates="tips", secondary=process_tiprole) - tiproleproceduretypeassociation = relationship( - "ProcedureTypeTipRoleAssociation", - back_populates="tiprole", - cascade="all, delete-orphan" - ) #: associated procedure - - proceduretype = association_proxy("tiproleproceduretypeassociation", "proceduretype", - creator=lambda proceduretype: ProcedureTypeTipRoleAssociation( - proceduretype=proceduretype)) + # tiproleproceduretypeassociation = relationship( + # "ProcedureTypeTipRoleAssociation", + # back_populates="tiprole", + # cascade="all, delete-orphan" + # ) #: associated procedure + # + # proceduretype = association_proxy("tiproleproceduretypeassociation", "proceduretype", + # creator=lambda proceduretype: ProcedureTypeTipRoleAssociation( + # proceduretype=proceduretype)) # @classmethod # def query_or_create(cls, **kwargs) -> Tuple[TipRole, bool]: @@ -2990,35 +2865,37 @@ class TipRole(BaseClass): ) -class Tips(BaseClass, LogMixin): +class TipsLot(BaseClass, LogMixin): """ A concrete instance of tips. """ id = Column(INTEGER, primary_key=True) #: primary key - tiprole = relationship("TipRole", back_populates="tips", + tips = relationship("Tips", back_populates="tipslot", secondary=tiprole_tips) #: joined parent reagent type - tiprole_id = Column(INTEGER, ForeignKey("_tiprole.id", ondelete='SET NULL', - name="fk_tip_role_id")) #: id of parent reagent type - manufacturer = Column(String(64)) - capacity = Column(INTEGER) - ref = Column(String(64)) #: tip reference number - # lot = Column(String(64)) #: lot number of tips - equipment = relationship("Equipment", back_populates="tips", - secondary=equipment_tips) #: associated procedure - tipsprocedureassociation = relationship( - "ProcedureTipsAssociation", - back_populates="tips", - cascade="all, delete-orphan" - ) #: associated procedure + tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete='SET NULL', + name="fk_tips_id")) #: id of parent reagent type + lot = Column(String(64), unique=True) + expiry = Column(TIMESTAMP) - procedure = association_proxy("tipsprocedureassociation", 'procedure') + # lot = Column(String(64)) #: lot number of tips + # equipment = relationship("Equipment", back_populates="tips", + # secondary=equipment_tips) #: associated procedure + # tipsprocedureassociation = relationship( + # "ProcedureTipsAssociation", + # back_populates="tips", + # cascade="all, delete-orphan" + # ) #: associated procedure + # + # procedure = association_proxy("tipsprocedureassociation", 'procedure') + + procedureequipmentassociation = @property def size(self) -> str: return f"{self.capacity}ul" @property - def name (self) -> str: + def name(self) -> str: return f"{self.manufacturer}-{self.size}-{self.ref}" # @classmethod @@ -3121,6 +2998,187 @@ class Tips(BaseClass, LogMixin): return output +class ProcedureEquipmentAssociation(BaseClass): + """ + Abstract association between BasicRun and Equipment + """ + + equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment + procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure + equipmentrole = Column(String(64), primary_key=True) #: name of the reagentrole the equipment fills + processversion_id = Column(INTEGER, ForeignKey("_processversion.id", ondelete="SET NULL", + name="SEA_Process_id")) #: Foreign key of process id + start_time = Column(TIMESTAMP) #: start time of equipment use + end_time = Column(TIMESTAMP) #: end time of equipment use + comments = Column(String(1024)) #: comments about equipment + + procedure = relationship(Procedure, + back_populates="procedureequipmentassociation") #: associated procedure + + equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment + + processversion = relationship(ProcessVersion) + + tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete="SET NULL", + name="SEA_Process_id")) + + tips = relationship(Tips) + + def __repr__(self) -> str: + try: + return f"" + except AttributeError: + return "" + + def __init__(self, procedure=None, equipment=None, procedure_id: int | None = None, equipment_id: int | None = None, + equipmentrole: str = "None"): + if not procedure: + if procedure_id: + procedure = Procedure.query(id=procedure_id) + else: + logger.error("Creation error") + self.procedure = procedure + if not equipment: + if equipment_id: + equipment = Equipment.query(id=equipment_id) + else: + logger.error("Creation error") + self.equipment = equipment + if isinstance(equipmentrole, list): + equipmentrole = equipmentrole[0] + if isinstance(equipmentrole, EquipmentRole): + equipmentrole = equipmentrole.name + self.equipmentrole = equipmentrole + + @property + def name(self): + return f"{self.procedure.name} & {self.equipment.name}" + + @property + def process(self): + return ProcessVersion.query(id=self.processversion_id) + + @property + def tips(self): + try: + return Tips.query(id=self.tips_id, limit=1) + except AttributeError: + return None + + def to_sub_dict(self) -> dict: + """ + This RunEquipmentAssociation as a dictionary + + Returns: + dict: This RunEquipmentAssociation as a dictionary + """ + try: + process = self.process.name + except AttributeError: + process = "No process found" + output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, + processes=[process], role=self.equipmentrole, nickname=self.equipment.nickname) + return output + + def to_pydantic(self) -> "PydEquipment": + """ + Returns a pydantic model based on this object. + + Returns: + PydEquipment: pydantic equipment model + """ + from backend.validators import PydEquipment + return PydEquipment(**self.details_dict()) + + @classmethod + @setup_lookup + def query(cls, + equipment: int | Equipment | None = None, + procedure: int | Procedure | None = None, + equipmentrole: str | None = None, + limit: int = 0, **kwargs) \ + -> Any | List[Any]: + query: Query = cls.__database_session__.query(cls) + match equipment: + case int(): + query = query.filter(cls.equipment_id == equipment) + case Equipment(): + query = query.filter(cls.equipment == equipment) + case _: + pass + match procedure: + case int(): + query = query.filter(cls.procedure_id == procedure) + case Procedure(): + query = query.filter(cls.procedure == procedure) + case _: + pass + if equipmentrole is not None: + query = query.filter(cls.equipmentrole == equipmentrole) + return cls.execute_query(query=query, limit=limit, **kwargs) + + def details_dict(self, **kwargs): + output = super().details_dict() + # NOTE: Figure out how to merge the misc_info if doing .update instead. + relevant = {k: v for k, v in output.items() if k not in ['equipment']} + output = output['equipment'].details_dict() + misc = output['misc_info'] + output.update(relevant) + output['misc_info'] = misc + output['equipment_role'] = self.equipmentrole + output['processversion'] = self.processversion.details_dict() + try: + output['tips'] = self.tips.details_dict() + except AttributeError: + output['tips'] = None + return output + + +class ProcedureTypeEquipmentRoleAssociation(BaseClass): + """ + Abstract association between SubmissionType and EquipmentRole + """ + equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment + proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), + primary_key=True) #: id of associated procedure + # kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), + # primary_key=True) + uses = Column(JSON) #: locations of equipment on the procedure type excel sheet. + static = Column(INTEGER, + default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list? + + proceduretype = relationship(ProcedureType, + back_populates="proceduretypeequipmentroleassociation") #: associated procedure + + equipmentrole = relationship(EquipmentRole, + back_populates="equipmentroleproceduretypeassociation") #: associated equipment + + + + @validates('static') + def validate_static(self, key, value): + """ + Ensures only 1 & 0 used in 'static' + + Args: + key (str): name of attribute + value (_type_): value of attribute + + Raises: + ValueError: Raised if bad value given + + Returns: + _type_: value + """ + if not 0 <= value < 2: + raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') + return value + + @check_authorization + def save(self): + super().save() + + class ProcedureTypeTipRoleAssociation(BaseClass): """ Abstract association between SubmissionType and TipRole @@ -3144,71 +3202,71 @@ class ProcedureTypeTipRoleAssociation(BaseClass): pass -class ProcedureTipsAssociation(BaseClass): - """ - Association between a concrete procedure instance and concrete tips - """ - tips_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment - procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure - procedure = relationship("Procedure", - back_populates="proceduretipsassociation") #: associated procedure - tips = relationship(Tips, - back_populates="tipsprocedureassociation") #: associated equipment - tiprole = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) - - def to_sub_dict(self) -> dict: - """ - This item as a dictionary - - Returns: - dict: Values of this object - """ - return dict(role=self.role_name, name=self.tips.name, lot=self.tips.lot) - - @classmethod - @setup_lookup - def query(cls, tips: int | Tips, tiprole: str, procedure: int | Procedure | None = None, limit: int = 0, **kwargs) \ - -> Any | List[Any]: - query: Query = cls.__database_session__.query(cls) - match tips: - case int(): - query = query.filter(cls.tips_id == tips) - case Tips(): - query = query.filter(cls.tips == tips) - case _: - pass - match procedure: - case int(): - query = query.filter(cls.procedure_id == procedure) - case Procedure(): - query = query.filter(cls.procedure == procedure) - case _: - pass - query = query.filter(cls.tiprole == tiprole) - return cls.execute_query(query=query, limit=limit, **kwargs) - - # TODO: fold this into the BaseClass.query_or_create ? - # @classmethod - # def query_or_create(cls, tips, procedure, role: str, **kwargs): - # kwargs['limit'] = 1 - # instance = cls.query(tips_id=tips.id, role_name=role, procedure_id=procedure.id, **kwargs) - # if instance is None: - # instance = cls(procedure=procedure, tips=tips, role_name=role) - # return instance - - def to_pydantic(self): - from backend.validators import PydTips - return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name) - - def details_dict(self, **kwargs): - output = super().details_dict() - # NOTE: Figure out how to merge the misc_info if doing .update instead. - relevant = {k: v for k, v in output.items() if k not in ['tips']} - output = output['tips'].details_dict() - misc = output['misc_info'] - output.update(relevant) - output['misc_info'] = misc - return output +# class ProcedureTipsAssociation(BaseClass): +# """ +# Association between a concrete procedure instance and concrete tips +# """ +# tips_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment +# procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure +# procedure = relationship("Procedure", +# back_populates="proceduretipsassociation") #: associated procedure +# tips = relationship(Tips, +# back_populates="tipsprocedureassociation") #: associated equipment +# tiprole = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) +# +# def to_sub_dict(self) -> dict: +# """ +# This item as a dictionary +# +# Returns: +# dict: Values of this object +# """ +# return dict(role=self.role_name, name=self.tips.name, lot=self.tips.lot) +# +# @classmethod +# @setup_lookup +# def query(cls, tips: int | Tips, tiprole: str, procedure: int | Procedure | None = None, limit: int = 0, **kwargs) \ +# -> Any | List[Any]: +# query: Query = cls.__database_session__.query(cls) +# match tips: +# case int(): +# query = query.filter(cls.tips_id == tips) +# case Tips(): +# query = query.filter(cls.tips == tips) +# case _: +# pass +# match procedure: +# case int(): +# query = query.filter(cls.procedure_id == procedure) +# case Procedure(): +# query = query.filter(cls.procedure == procedure) +# case _: +# pass +# query = query.filter(cls.tiprole == tiprole) +# return cls.execute_query(query=query, limit=limit, **kwargs) +# +# # TODO: fold this into the BaseClass.query_or_create ? +# # @classmethod +# # def query_or_create(cls, tips, procedure, role: str, **kwargs): +# # kwargs['limit'] = 1 +# # instance = cls.query(tips_id=tips.id, role_name=role, procedure_id=procedure.id, **kwargs) +# # if instance is None: +# # instance = cls(procedure=procedure, tips=tips, role_name=role) +# # return instance +# +# def to_pydantic(self): +# from backend.validators import PydTips +# return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name) +# +# def details_dict(self, **kwargs): +# output = super().details_dict() +# # NOTE: Figure out how to merge the misc_info if doing .update instead. +# relevant = {k: v for k, v in output.items() if k not in ['tips']} +# output = output['tips'].details_dict() +# misc = output['misc_info'] +# output.update(relevant) +# output['misc_info'] = misc +# return output class Results(BaseClass): diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 89b9ce5..6fc0dea 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1248,6 +1248,7 @@ class Run(BaseClass, LogMixin): dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self)) if dlg.exec(): sql, _ = dlg.return_sql(new=True) + # sys.exit(pformat(sql.__dict__)) sql.save() obj.set_data() @@ -1460,11 +1461,11 @@ class Run(BaseClass, LogMixin): return list(sorted(padded_list, key=itemgetter('submission_rank'))) -class SampleType(BaseClass): - id = Column(INTEGER, primary_key=True) #: primary key - name = Column(String(64), nullable=False, unique=True) #: identification from submitter - - sample = relationship("Sample", back_populates="sampletype", uselist=True) +# class SampleType(BaseClass): +# id = Column(INTEGER, primary_key=True) #: primary key +# name = Column(String(64), nullable=False, unique=True) #: identification from submitter +# +# sample = relationship("Sample", back_populates="sampletype", uselist=True) # NOTE: Sample Classes @@ -1476,9 +1477,9 @@ class Sample(BaseClass, LogMixin): id = Column(INTEGER, primary_key=True) #: primary key sample_id = Column(String(64), nullable=False, unique=True) #: identification from submitter - sampletype_id = Column(INTEGER, ForeignKey("_sampletype.id", ondelete="SET NULL", - name="fk_SAMP_sampletype_id")) - sampletype = relationship("SampleType", back_populates="sample") + # sampletype_id = Column(INTEGER, ForeignKey("_sampletype.id", ondelete="SET NULL", + # name="fk_SAMP_sampletype_id")) + # sampletype = relationship("SampleType", back_populates="sample") # misc_info = Column(JSON) control = relationship("Control", back_populates="sample", uselist=False) @@ -1512,10 +1513,7 @@ class Sample(BaseClass, LogMixin): return self.sample_id def __repr__(self) -> str: - try: - return f"<{self.sampletype.name.replace('_', ' ').title().replace(' ', '')}({self.sample_id})>" - except AttributeError: - return f"" + return f"" @classproperty def searchables(cls): @@ -1531,13 +1529,13 @@ class Sample(BaseClass, LogMixin): Returns: dict: submitter id and sample type and linked procedure if full data """ - try: - sample_type = self.sampletype.name - except AttributeError: - sample_type = "NA" + # try: + # sample_type = self.sampletype.name + # except AttributeError: + # sample_type = "NA" sample = dict( - sample_id=self.sample_id, - sampletype=sample_type + sample_id=self.sample_id + # sampletype=sample_type ) if full_data: sample['clientsubmission'] = sorted([item.to_sub_dict() for item in self.sampleclientsubmissionassociation], @@ -1565,7 +1563,7 @@ class Sample(BaseClass, LogMixin): @setup_lookup def query(cls, sample_id: str | None = None, - sampletype: str | SampleType | None = None, + # sampletype: str | SampleType | None = None, limit: int = 0, **kwargs ) -> Sample | List[Sample]: @@ -1574,20 +1572,19 @@ class Sample(BaseClass, LogMixin): Args: sample_id (str | None, optional): Name of the sample (limits results to 1). Defaults to None. - sampletype (str | None, optional): Sample type. Defaults to None. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. Returns: models.Sample|List[models.Sample]: Sample(s) of interest. """ query = cls.__database_session__.query(cls) - match sampletype: - case str(): - query = query.join(SampleType).filter(SampleType.name == sampletype) - case SampleType(): - query = query.filter(cls.sampletype == sampletype) - case _: - pass + # match sampletype: + # case str(): + # query = query.join(SampleType).filter(SampleType.name == sampletype) + # case SampleType(): + # query = query.filter(cls.sampletype == sampletype) + # case _: + # pass match sample_id: case str(): query = query.filter(cls.sample_id == sample_id) @@ -1596,37 +1593,37 @@ class Sample(BaseClass, LogMixin): pass return cls.execute_query(query=query, limit=limit, **kwargs) - @classmethod - def fuzzy_search(cls, - sampletype: str | Sample | None = None, - **kwargs - ) -> List[Sample]: - """ - Allows for fuzzy search of sample. - - Args: - sampletype (str | BasicSample | None, optional): Type of sample. Defaults to None. - - Returns: - List[Sample]: List of sample that match kwarg search parameters. - """ - query: Query = cls.__database_session__.query(cls) - match sampletype: - case str(): - query = query.join(SampleType).filter(SampleType.name == sampletype) - case SampleType(): - query = query.filter(cls.sampletype == sampletype) - case _: - pass - for k, v in kwargs.items(): - search = f"%{v}%" - try: - attr = getattr(cls, k) - # NOTE: the secret sauce is in attr.like - query = query.filter(attr.like(search)) - except (ArgumentError, AttributeError) as e: - logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.") - return query.limit(50).all() + # @classmethod + # def fuzzy_search(cls, + # sampletype: str | Sample | None = None, + # **kwargs + # ) -> List[Sample]: + # """ + # Allows for fuzzy search of sample. + # + # Args: + # sampletype (str | BasicSample | None, optional): Type of sample. Defaults to None. + # + # Returns: + # List[Sample]: List of sample that match kwarg search parameters. + # """ + # query: Query = cls.__database_session__.query(cls) + # match sampletype: + # case str(): + # query = query.join(SampleType).filter(SampleType.name == sampletype) + # case SampleType(): + # query = query.filter(cls.sampletype == sampletype) + # case _: + # pass + # for k, v in kwargs.items(): + # search = f"%{v}%" + # try: + # attr = getattr(cls, k) + # # NOTE: the secret sauce is in attr.like + # query = query.filter(attr.like(search)) + # except (ArgumentError, AttributeError) as e: + # logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.") + # return query.limit(50).all() def delete(self): raise AttributeError(f"Delete not implemented for {self.__class__}") diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 751b225..9340bd9 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -35,6 +35,7 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True): @model_validator(mode="before") @classmethod def prevalidate(cls, data): + # logger.debug(f"Initial data for {cls.__name__}: {pformat(data)}") sql_fields = [k for k, v in cls._sql_object.__dict__.items() if isinstance(v, InstrumentedAttribute)] output = {} try: @@ -72,7 +73,7 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True): def __init__(self, **data): # NOTE: Grab the sql model for validation purposes. # self.__class__._sql_object = getattr(models, self.__class__.__name__.replace("Pyd", "")) - logger.debug(f"Initial data: {data}") + super().__init__(**data) def filter_field(self, key: str) -> Any: @@ -284,7 +285,7 @@ class PydReagent(PydBaseClass): class PydSample(PydBaseClass): sample_id: str - sampletype: str | None = Field(default=None) + # sampletype: str | None = Field(default=None) submission_rank: int | List[int] | None = Field(default=0, validate_default=True) enabled: bool = Field(default=True) row: int = Field(default=0) @@ -327,7 +328,7 @@ class PydSample(PydBaseClass): def improved_dict(self, dictionaries: bool = True) -> dict: output = super().improved_dict(dictionaries=dictionaries) output['name'] = self.sample_id - del output['sampletype'] + # del output['sampletype'] return output def to_sql(self): @@ -399,22 +400,24 @@ class PydEquipment(PydBaseClass): @field_validator('process', mode='before') @classmethod - def make_empty_list(cls, value): + def make_empty_list(cls, value, values): # if isinstance(value, dict): # value = value['processes'] if isinstance(value, GeneratorType): value = [item for item in value] value = convert_nans_to_nones(value) if not value: - value = [''] - # logger.debug(value) + value = [] + logger.debug(value) try: # value = [item.strip() for item in value] - d = next((process for process in value), None) - logger.debug(f"Next process: {d.detail_dict()}") - value = PydProcess(d.details_dict()) + d: Process = next((process for process in value if values.data['name'] in [item.name for item in process.equipment]), None) + # logger.debug(f"Next process: {d.details_dict()}") + value = d.to_pydantic() + # value = PydProcess(**d.details_dict()) # value = next((process.to_pydantic() for process in value)) - except AttributeError: + except AttributeError as e: + logger.error(f"Process Validation error due to {e}") pass return value @@ -1179,6 +1182,7 @@ class PydProcess(PydBaseClass, extra="allow"): @field_validator("proceduretype", "equipment", "equipmentrole", "tiprole", mode="before") @classmethod def enforce_list(cls, value): + # logger.debug(f"Validating field: {value}") if not isinstance(value, list): value = [value] output = [] @@ -1192,32 +1196,36 @@ class PydProcess(PydBaseClass, extra="allow"): @report_result def to_sql(self): report = Report() - instance = Process.query(name=self.name) + logger.debug(f"Query process: {self.name}, version = {self.version}") + # NOTE: can't use query_or_create due to name not being part of ProcessVersion + instance = ProcessVersion.query(name=self.name, version=self.version, limit=1) + logger.debug(f"Got instance: {instance}") if not instance: - instance = Process() - fields = [item for item in self.model_fields] - for field in fields: - logger.debug(f"Field: {field}") - try: - field_type = getattr(instance.__class__, field).property - except AttributeError: - logger.error(f"No attribute: {field} in {instance.__class__}") - continue - match field_type: - case _RelationshipDeclared(): - logger.debug(f"{field} is a relationship with {field_type.entity.class_}") - query_str = getattr(self, field) - if isinstance(query_str, list): - query_str = query_str[0] - if query_str in ["", " ", None]: - continue - logger.debug(f"Querying {field_type.entity.class_} with name {query_str}") - field_value = field_type.entity.class_.query(name=query_str) - logger.debug(f"{field} query result: {field_value}") - case ColumnProperty(): - logger.debug(f"{field} is a property.") - field_value = getattr(self, field) - instance.set_attribute(key=field, value=field_value) + instance = ProcessVersion() + # fields = [item for item in self.model_fields] + # for field in fields: + # logger.debug(f"Field: {field}") + # try: + # field_type = getattr(instance.__class__, field).property + # except AttributeError: + # logger.error(f"No attribute: {field} in {instance.__class__}") + # continue + # match field_type: + # case _RelationshipDeclared(): + # logger.debug(f"{field} is a relationship with {field_type.entity.class_}") + # query_str = getattr(self, field) + # if isinstance(query_str, list): + # query_str = query_str[0] + # if query_str in ["", " ", None]: + # continue + # logger.debug(f"Querying {field_type.entity.class_} with name {query_str}") + # field_value = field_type.entity.class_.query(name=query_str) + # logger.debug(f"{field} query result: {field_value}") + # case ColumnProperty(): + # logger.debug(f"{field} is a property.") + # field_value = getattr(self, field) + # # instance.set_attribute(key=field, value=field_value) + # setattr(instance, field, field_value) return instance, report @@ -1469,7 +1477,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): idx = 0 insertable = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) self.reagent.insert(idx, insertable) - logger.debug(self.reagent) + # logger.debug(self.reagent) @classmethod def update_new_reagents(cls, reagent: PydReagent): @@ -1506,9 +1514,9 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): sql.proceduretype = self.proceduretype # Note: convert any new reagents to sql and save # for reagentrole, reagents in self.reagentrole.items(): - for reagent in self.reagent: - if not reagent.lot or reagent.name == "--New--": - continue + # for reagent in self.reagent: + # if not reagent.lot or reagent.name == "--New--": + # continue # self.update_new_reagents(reagent) # NOTE: reset reagent associations. # sql.procedurereagentassociation = [] @@ -1532,7 +1540,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): r.delete() else: removable.delete() - # logger.debug(f"Adding {reagent} to {sql}") + logger.debug(f"Adding {reagent} to {sql}") reagent_assoc = ProcedureReagentLotAssociation(reagentlot=reagent, procedure=sql, reagentrole=reagentrole) try: start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 @@ -1556,14 +1564,18 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql, row=sample.row, column=sample.column, procedure_rank=sample.procedure_rank) - sys.exit(pformat(self.equipment)) + # sys.exit(pformat(self.equipment)) for equipment in self.equipment: - equip = Equipment.query(name=equipment.name) + logger.debug(f"Equipment: {equipment}") + # equip = Equipment.query(name=equipment.name) + equip, _ = equipment.to_sql() + logger.debug(f"Process: {equipment.process}") if equip not in sql.equipment: equip_assoc = ProcedureEquipmentAssociation(equipment=equip, procedure=sql, equipmentrole=equip.equipmentrole[0]) process = equipment.process.to_sql() - equip_assoc.process = process + equip_assoc.processversion = process + # sys.exit(pformat([item.__dict__ for item in sql.procedureequipmentassociation])) return sql, None diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index d646c85..5837424 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -134,7 +134,7 @@ class ProcedureCreation(QDialog): tips = next((tps for tps in equipment.tips if tps.name == tips), None) if tips: eoi.tips = tips.to_pydantic() - self.procedure.equipment.append(eoi) + self.procedure.equipment.append(eoi) logger.debug(f"Updated equipment: {self.procedure.equipment}") @pyqtSlot(str, str) diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index aa3cd6b..48cf1cf 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -110,7 +110,7 @@ class SubmissionDetails(QDialog): @pyqtSlot(str) def process_details(self, process: str | Process): - logger.debug(f"Equipment details") + logger.debug(f"Process details") if isinstance(process, str): process = Process.query(name=process) base_dict = process.to_sub_dict(full_data=True)