diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 81a870c..249ef2d 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -234,10 +234,9 @@ 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) - and not isinstance(v.property, _RelationshipDeclared)] + allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute)] + # and not isinstance(v.property, _RelationshipDeclared)] sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed} - logger.debug(f"Sanitized kwargs: {sanitized_kwargs}") instance = cls.query(**sanitized_kwargs) if not instance or isinstance(instance, list): @@ -554,6 +553,8 @@ 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): + dicto = {k:v for k,v in self.__dict__.items() if not k.startswith("_")} diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 07a2ae1..ff001ab 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1082,7 +1082,7 @@ class ProcedureType(BaseClass): equipment = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole", creator=lambda eq: ProcedureTypeEquipmentRoleAssociation( - equipment_role=eq)) #: Proxy of equipmentrole associations + equipmentrole=eq)) #: Proxy of equipmentrole associations kittypereagentroleassociation = relationship( "KitTypeReagentRoleAssociation", @@ -1330,10 +1330,34 @@ class Procedure(BaseClass): rs = results_class(procedure=self, parent=obj) def add_equipment(self, obj): + """ + Creates widget for adding equipment to this submission + + Args: + obj (_type_): parent widget + """ + logger.debug(f"Add equipment") from frontend.widgets.equipment_usage import EquipmentUsage dlg = EquipmentUsage(parent=obj, procedure=self) if dlg.exec(): - pass + equipment = dlg.parse_form() + for equip in equipment: + logger.debug(f"Parsed equipment: {equip}") + _, assoc = equip.to_sql(procedure=self) + logger.debug(f"Got equipment association: {assoc} for {equip}") + try: + assoc.save() + except AttributeError as e: + logger.error(f"Couldn't save association with {equip} due to {e}") + if equip.tips: + for tips in equip.tips: + # logger.debug(f"Attempting to add tips assoc: {tips} (pydantic)") + tassoc, _ = tips.to_sql(procedure=self) + # logger.debug(f"Attempting to add tips assoc: {tips.__dict__} (sql)") + if tassoc not in self.proceduretipsassociation: + tassoc.save() + else: + logger.error(f"Tips already found in submission, skipping.") def edit(self, obj): logger.debug("Edit!") @@ -1348,8 +1372,6 @@ class Procedure(BaseClass): logger.debug("Delete!") - - class ProcedureTypeKitTypeAssociation(BaseClass): """ Abstract of relationship between kits and their procedure type. @@ -1860,7 +1882,7 @@ class Equipment(BaseClass, LogMixin): proceduretype = ProcedureType.query(name=proceduretype) if isinstance(kittype, str): kittype = KitType.query(name=kittype) - for process in self.processes: + for process in self.process: if proceduretype not in process.proceduretype: continue if kittype and kittype not in process.kittype: @@ -1872,6 +1894,7 @@ class Equipment(BaseClass, LogMixin): @classmethod @setup_lookup def query(cls, + id: int | None = None, name: str | None = None, nickname: str | None = None, asset_number: str | None = None, @@ -1890,6 +1913,12 @@ class Equipment(BaseClass, LogMixin): Equipment|List[Equipment]: Equipment or list of equipment matching query parameters. """ query = cls.__database_session__.query(cls) + match id: + case int(): + query = query.filter(cls.id == id) + limit = 1 + case _: + pass match name: case str(): query = query.filter(cls.name == name) @@ -1925,7 +1954,8 @@ class Equipment(BaseClass, LogMixin): from backend.validators.pydant import PydEquipment processes = self.get_processes(proceduretype=proceduretype, kittype=kittype, equipmentrole=equipmentrole) - return PydEquipment(processes=processes, role=equipmentrole, + logger.debug(f"EquipmentRole: {equipmentrole}") + return PydEquipment(processes=processes, equipmentrole=equipmentrole, **self.to_dict(processes=False)) @classproperty @@ -2046,7 +2076,7 @@ class EquipmentRole(BaseClass): Returns: dict: This EquipmentRole dict """ - return {key: value for key, value in self.__dict__.items() if key != "process"} + return {key: value for key, value in self.__dict__.items() if key != "process" and key != "equipment"} def to_pydantic(self, proceduretype: ProcedureType, kittype: str | KitType | None = None) -> "PydEquipmentRole": @@ -2061,7 +2091,7 @@ class EquipmentRole(BaseClass): PydEquipmentRole: This EquipmentRole as PydEquipmentRole """ from backend.validators.pydant import PydEquipmentRole - equipment = [item.to_pydantic(proceduretype=proceduretype, kittype=kittype) for item in + equipment = [item.to_pydantic(proceduretype=proceduretype, kittype=kittype, equipmentrole=self) for item in self.equipment] pyd_dict = self.to_dict() pyd_dict['process'] = self.get_processes(proceduretype=proceduretype, kittype=kittype) @@ -2131,7 +2161,7 @@ class EquipmentRole(BaseClass): proceduretype = SubmissionType.query(name=proceduretype) if isinstance(kittype, str): kittype = KitType.query(name=kittype) - for process in self.processes: + for process in self.process: if proceduretype and proceduretype not in process.proceduretype: continue if kittype and kittype not in process.kittype: @@ -2163,10 +2193,23 @@ class ProcedureEquipmentAssociation(BaseClass): equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment def __repr__(self) -> str: - return f"" + try: + return f"" + except AttributeError: + return "" - def __init__(self, procedure, equipment, equipmentrole: str = "None"): - self.run = procedure + 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 self.equipmentrole = equipmentrole @@ -2201,12 +2244,27 @@ class ProcedureEquipmentAssociation(BaseClass): @classmethod @setup_lookup - def query(cls, equipment_id: int | None = None, run_id: int | None = None, equipmentrole: str | None = None, + 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) - query = query.filter(cls.equipment_id == equipment_id) - query = query.filter(cls.run_id == run_id) + 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) @@ -2632,7 +2690,7 @@ class ProcedureTipsAssociation(BaseClass): back_populates="proceduretipsassociation") #: associated procedure tips = relationship(Tips, back_populates="tipsprocedureassociation") #: associated equipment - role_name = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) + tiprole = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) def to_sub_dict(self) -> dict: """ @@ -2645,23 +2703,34 @@ class ProcedureTipsAssociation(BaseClass): @classmethod @setup_lookup - def query(cls, tips_id: int, role_name: str, procedure_id: int | None = None, limit: int = 0, **kwargs) \ + 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) - query = query.filter(cls.tips_id == tips_id) - if procedure_id is not None: - query = query.filter(cls.procedure_id == procedure_id) - query = query.filter(cls.role_name == role_name) + 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, run, role: str, **kwargs): - kwargs['limit'] = 1 - instance = cls.query(tips_id=tips.id, role_name=role, procedure_id=run.id, **kwargs) - if instance is None: - instance = cls(run=run, tips=tips, role_name=role) - return instance + # @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 diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 4a4e844..b7d2640 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -289,7 +289,8 @@ class PydTips(BaseModel): report = Report() tips = Tips.query(name=self.name, limit=1) # logger.debug(f"Tips query has yielded: {tips}") - assoc = ProcedureTipsAssociation.query_or_create(tips=tips, procedure=procedure, role=self.role, limit=1) + assoc = ProcedureTipsAssociation.query_or_create(tips=tips, procedure=procedure, tiprole=self.tiprole, limit=1) + logger.debug(f"Got association: {assoc}") return assoc, report @@ -297,7 +298,7 @@ class PydEquipment(BaseModel, extra='ignore'): asset_number: str name: str nickname: str | None - process: List[str] | None + processes: List[str] | None equipmentrole: str | None tips: List[PydTips] | None = Field(default=None) @@ -308,9 +309,11 @@ class PydEquipment(BaseModel, extra='ignore'): value = value.name return value - @field_validator('process', mode='before') + @field_validator('processes', mode='before') @classmethod def make_empty_list(cls, value): + # if isinstance(value, dict): + # value = value['processes'] if isinstance(value, GeneratorType): value = [item.name for item in value] value = convert_nans_to_nones(value) @@ -339,20 +342,21 @@ class PydEquipment(BaseModel, extra='ignore'): procedure = Procedure.query(name=procedure) if isinstance(kittype, str): kittype = KitType.query(name=kittype) + logger.debug(f"Querying equipment: {self.asset_number}") equipment = Equipment.query(asset_number=self.asset_number) if equipment is None: logger.error("No equipment found. Returning None.") - return + return None, None if procedure is not None: # NOTE: Need to make sure the same association is not added to the procedure try: - assoc = ProcedureEquipmentAssociation.query(equipment_id=equipment.id, submission_id=procedure.id, + assoc, new = ProcedureEquipmentAssociation.query_or_create(equipment=equipment, procedure=procedure, equipmentrole=self.equipmentrole, limit=1) except TypeError as e: logger.error(f"Couldn't get association due to {e}, returning...") - assoc = None - if assoc is None: - assoc = ProcedureEquipmentAssociation(submission=procedure, equipment=equipment) + return None, None + if new: + # assoc = ProcedureEquipmentAssociation(procedure=procedure, equipment=equipment) # TODO: This seems precarious. What if there is more than one process? # NOTE: It looks like the way fetching the process is done in the SQL model, this shouldn't be a problem, but I'll include a failsafe. # NOTE: I need to find a way to filter this by the kittype involved. @@ -365,7 +369,7 @@ class PydEquipment(BaseModel, extra='ignore'): logger.error(f"Found unknown process: {process}.") # logger.debug(f"Using process: {process}") assoc.process = process - assoc.equipmentrole = self.role + assoc.equipmentrole = self.equipmentrole else: logger.warning(f"Found already existing association: {assoc}") assoc = None diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index 4ea635d..94b7b4f 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -124,9 +124,10 @@ class RoleComboBox(QWidget): """ equip = self.box.currentText() equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0]) + logger.debug(f"Equip2: {equip2}") with QSignalBlocker(self.process) as blocker: self.process.clear() - self.process.addItems([item for item in equip2.processes if item in self.role.processes]) + self.process.addItems([item for item in equip2.processes if item in self.role.process]) def update_tips(self): """ @@ -137,7 +138,7 @@ class RoleComboBox(QWidget): if process.tiprole: for iii, tip_role in enumerate(process.tiprole): widget = QComboBox() - tip_choices = [item.name for item in tip_role.control] + tip_choices = [item.name for item in tip_role.tips] widget.setEditable(False) widget.addItems(tip_choices) widget.setObjectName(f"tips_{tip_role.name}") @@ -162,13 +163,13 @@ class RoleComboBox(QWidget): PydEquipment|None: PydEquipment matching form """ eq = Equipment.query(name=self.box.currentText()) - tips = [PydTips(name=item.currentText(), role=item.objectName().lstrip("tips").lstrip("_"), lot="") for item in + tips = [PydTips(name=item.currentText(), tiprole=item.objectName().lstrip("tips").lstrip("_"), lot="") for item in self.findChildren(QComboBox) if item.objectName().startswith("tips")] try: return PydEquipment( name=eq.name, processes=[self.process.currentText().strip()], - role=self.role.name, + equipmentrole=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname, tips=tips diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index a450154..0ba0f12 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -311,7 +311,10 @@ class SubmissionsTree(QTreeView): 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()}") + try: + logger.debug(f"Model: {event.model().event()}") + except TypeError as e: + logger.error(f"Couldn't expand due to {e}") def contextMenuEvent(self, event: QContextMenuEvent): """ @@ -330,6 +333,7 @@ class SubmissionsTree(QTreeView): # clientsubmission = id.model().query_group_object(id.row()) self.menu = QMenu(self) self.con_actions = query_obj.custom_context_events + logger.debug(f"Context menu actions: {self.con_actions}") for key in self.con_actions.keys(): logger.debug(key) match key.lower():