From e0e3080af01372216d427805d7eadbb32bbeb5cb Mon Sep 17 00:00:00 2001 From: lwark Date: Thu, 13 Jun 2024 09:11:53 -0500 Subject: [PATCH] Tips parser complete. --- src/submissions/backend/db/models/kits.py | 118 +++++++++++++++--- .../backend/db/models/submissions.py | 30 ++++- src/submissions/backend/excel/parser.py | 118 +++++++++++++++--- .../backend/validators/__init__.py | 2 +- src/submissions/backend/validators/pydant.py | 35 ++++-- .../frontend/widgets/equipment_usage.py | 68 +++++++--- .../templates/basicsubmission_details.html | 6 + 7 files changed, 311 insertions(+), 66 deletions(-) diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index d05fa36..19419f1 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -8,13 +8,12 @@ from sqlalchemy.ext.associationproxy import association_proxy from datetime import date import logging, re from tools import check_authorization, setup_lookup, Report, Result -from typing import List, Literal +from typing import List, Literal, Any from pandas import ExcelFile from pathlib import Path from . import Base, BaseClass, Organization from io import BytesIO - logger = logging.getLogger(f'submissions.{__name__}') # logger.debug("Table for ReagentType/Reagent relations") @@ -79,14 +78,6 @@ tiproles_tips = Table( extend_existing=True ) -submissions_tips = Table( - "_submissions_tips", - Base.metadata, - Column("submission_id", INTEGER, ForeignKey("_basicsubmissions.id")), - Column("tips_id", INTEGER, ForeignKey("_tips.id")), - extend_existing=True -) - process_tiprole = Table( "_process_tiprole", Base.metadata, @@ -95,6 +86,15 @@ 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 +) + + class KitType(BaseClass): """ Base of kits used in submission processing @@ -133,7 +133,8 @@ class KitType(BaseClass): """ return f"" - def get_reagents(self, required: bool = False, submission_type: str | SubmissionType | None = None) -> List[ReagentRole]: + def get_reagents(self, required: bool = False, submission_type: str | SubmissionType | None = None) -> List[ + ReagentRole]: """ Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation. @@ -143,7 +144,7 @@ class KitType(BaseClass): Returns: List[ReagentRole]: List of reagents linked to this kit. - """ + """ match submission_type: case SubmissionType(): # logger.debug(f"Getting reagents by SubmissionType {submission_type}") @@ -609,6 +610,12 @@ class SubmissionType(BaseClass): cascade="all, delete-orphan" ) #: triple association of KitTypes, ReagentTypes, SubmissionTypes + submissiontype_tiprole_associations = relationship( + "SubmissionTypeTipRoleAssociation", + back_populates="submission_type", + cascade="all, delete-orphan" + ) + def __repr__(self) -> str: """ Returns: @@ -656,7 +663,7 @@ class SubmissionType(BaseClass): Returns: dict: Map of locations - """ + """ info = self.info_map # logger.debug(f"Info map: {info}") output = {} @@ -674,7 +681,7 @@ class SubmissionType(BaseClass): Returns: dict: sample location map - """ + """ return self.sample_map def construct_equipment_map(self) -> dict: @@ -693,6 +700,15 @@ class SubmissionType(BaseClass): output[item.equipment_role.name] = emap return output + def construct_tips_map(self): + output = {} + for item in self.submissiontype_tiprole_associations: + tmap = item.uses + if tmap is None: + tmap = {} + output[item.tip_role.name] = tmap + return output + def get_equipment(self, extraction_kit: str | KitType | None = None) -> List['PydEquipmentRole']: """ Returns PydEquipmentRole of all equipment associated with this SubmissionType @@ -735,7 +751,7 @@ class SubmissionType(BaseClass): Returns: BasicSubmission: Submission class - """ + """ from .submissions import BasicSubmission return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name) @@ -1071,6 +1087,8 @@ class Equipment(BaseClass): secondary=equipmentroles_equipment) #: relation to EquipmentRoles processes = relationship("Process", back_populates="equipment", secondary=equipment_processes) #: relation to Processes + tips = relationship("Tips", back_populates="equipment", + secondary=equipment_tips) #: relation to Processes equipment_submission_associations = relationship( "SubmissionEquipmentAssociation", back_populates="equipment", @@ -1467,7 +1485,7 @@ class Process(BaseClass): backref='process') #: relation to SubmissionEquipmentAssociation kit_types = relationship("KitType", back_populates='processes', secondary=kittypes_processes) #: relation to KitType - tip_roles = relationship("TipRoles", back_populates='processes', + tip_roles = relationship("TipRole", back_populates='processes', secondary=process_tiprole) #: relation to KitType def __repr__(self) -> str: @@ -1502,19 +1520,25 @@ class Process(BaseClass): class TipRole(BaseClass): - id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(64)) #: name of reagent type instances = relationship("Tips", back_populates="role", secondary=tiproles_tips) #: concrete instances of this reagent type processes = relationship("Process", back_populates="tip_roles", secondary=process_tiprole) + tiprole_submissiontype_associations = relationship( + "SubmissionTypeTipRoleAssociation", + back_populates="tip_role", + cascade="all, delete-orphan" + ) #: associated submission + + submission_types = association_proxy("tiprole_submissiontype_associations", "submission_type") def __repr__(self): return f"" -class Tips(BaseClass): +class Tips(BaseClass): id = Column(INTEGER, primary_key=True) #: primary key role = relationship("TipRole", back_populates="instances", secondary=tiproles_tips) #: joined parent reagent type @@ -1522,10 +1546,64 @@ class Tips(BaseClass): name="fk_tip_role_id")) #: id of parent reagent type name = Column(String(64)) #: tip common name lot = Column(String(64)) #: lot number of tips - submissions = relationship("BasicSubmission", back_populates="tips", - secondary=submissions_tips) #: associated submission + equipment = relationship("Equipment", back_populates="tips", + secondary=equipment_tips) #: associated submission + tips_submission_associations = relationship( + "SubmissionTipsAssociation", + back_populates="tips", + cascade="all, delete-orphan" + ) #: associated submission + + submissions = association_proxy("tips_submission_associations", 'submission') def __repr__(self): return f"" + @classmethod + def query(cls, name: str | None = None, lot: str | None = None, limit: int = 0, **kwargs) -> Any | List[Any]: + query = cls.__database_session__.query(cls) + match name: + case str(): + # logger.debug(f"Lookup Equipment by name str {name}") + query = query.filter(cls.name == name) + case _: + pass + match lot: + case str(): + # logger.debug(f"Lookup Equipment by nickname str {nickname}") + query = query.filter(cls.lot == lot) + limit = 1 + case _: + pass + return cls.execute_query(query=query, limit=limit) + +class SubmissionTypeTipRoleAssociation(BaseClass): + """ + Abstract association between SubmissionType and TipRole + """ + tiprole_id = Column(INTEGER, ForeignKey("_tiprole.id"), primary_key=True) #: id of associated equipment + submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"), + primary_key=True) #: id of associated submission + uses = Column(JSON) #: locations of equipment on the submission 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? + submission_type = relationship(SubmissionType, + back_populates="submissiontype_tiprole_associations") #: associated submission + tip_role = relationship(TipRole, + back_populates="tiprole_submissiontype_associations") #: associated equipment + + +class SubmissionTipsAssociation(BaseClass): + tip_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment + submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id"), primary_key=True) #: id of associated submission + submission = relationship("BasicSubmission", + back_populates="submission_tips_associations") #: associated submission + tips = relationship(Tips, + back_populates="tips_submission_associations") #: associated equipment + role_name = Column(String(32)) #, ForeignKey("_tiprole.name")) + + # role = relationship(TipRole) + + def to_sub_dict(self): + return dict(role=self.role_name, name=self.tips.name, lot=self.tips.lot) diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 1ed2a7c..a093e70 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -10,7 +10,7 @@ from zipfile import ZipFile from tempfile import TemporaryDirectory from operator import attrgetter, itemgetter from pprint import pformat -from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact +from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, Tips, TipRole, SubmissionTipsAssociation from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm.attributes import flag_modified @@ -96,6 +96,14 @@ class BasicSubmission(BaseClass): equipment = association_proxy("submission_equipment_associations", "equipment") #: Association proxy to SubmissionEquipmentAssociation.equipment + submission_tips_associations = relationship( + "SubmissionTipsAssociation", + back_populates="submission", + cascade="all, delete-orphan") + + tips = association_proxy("submission_tips_associations", + "tips") + # NOTE: Allows for subclassing into ex. BacterialCulture, Wastewater, etc. __mapper_args__ = { "polymorphic_identity": "Basic Submission", @@ -248,7 +256,6 @@ class BasicSubmission(BaseClass): ext_info = self.extraction_info except TypeError: ext_info = None - output = { "id": self.id, "plate_number": self.rsl_plate_num, @@ -282,16 +289,24 @@ class BasicSubmission(BaseClass): # logger.debug("Running equipment") try: equipment = [item.to_sub_dict() for item in self.submission_equipment_associations] - if len(equipment) == 0: + if not equipment: equipment = None except Exception as e: logger.error(f"Error setting equipment: {e}") equipment = None + try: + tips = [item.to_sub_dict() for item in self.submission_tips_associations] + if not tips: + tips = None + except Exception as e: + logger.error(f"Error setting tips: {e}") + tips = None cost_centre = self.cost_centre else: reagents = None samples = None equipment = None + tips = None cost_centre = None # logger.debug("Getting comments") try: @@ -315,6 +330,7 @@ class BasicSubmission(BaseClass): output["extraction_info"] = ext_info output["comment"] = comments output["equipment"] = equipment + output["tips"] = tips output["cost_centre"] = cost_centre output["signed_by"] = self.signed_by # logger.debug(f"Setting contact to: {contact} of type: {type(contact)}") @@ -440,7 +456,7 @@ class BasicSubmission(BaseClass): excluded = ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents', 'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls', 'source_plates', 'pcr_technician', 'ext_technician', 'artic_technician', 'cost_centre', - 'signed_by', 'artic_date', 'gel_barcode', 'gel_date', 'ngs_date', 'contact_phone', 'contact'] + 'signed_by', 'artic_date', 'gel_barcode', 'gel_date', 'ngs_date', 'contact_phone', 'contact', 'tips'] for item in excluded: try: df = df.drop(item, axis=1) @@ -1110,6 +1126,12 @@ class BasicSubmission(BaseClass): _, assoc = equip.toSQL(submission=self) # logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}") assoc.save() + if equip.tips: + logger.debug("We have tips in this equipment") + for tips in equip.tips: + tassoc = tips.to_sql(submission=self) + tassoc.save() + else: pass diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index b347543..cb78b18 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -10,7 +10,7 @@ import pandas as pd from openpyxl import load_workbook, Workbook from pathlib import Path from backend.db.models import * -from backend.validators import PydSubmission, PydReagent, RSLNamer, PydSample, PydEquipment +from backend.validators import PydSubmission, PydReagent, RSLNamer, PydSample, PydEquipment, PydTips import logging, re from collections import OrderedDict from datetime import date @@ -57,6 +57,7 @@ class SheetParser(object): self.parse_reagents() self.parse_samples() self.parse_equipment() + self.parse_tips() self.finalize_parse() # logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}") @@ -101,6 +102,10 @@ class SheetParser(object): parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type) self.sub['equipment'] = parser.parse_equipment() + def parse_tips(self): + parser = TipParser(xl=self.xl, submission_type=self.submission_type) + self.sub['tips'] = parser.parse_tips() + def import_kit_validation_check(self): """ Enforce that the parser has an extraction kit @@ -139,13 +144,21 @@ class SheetParser(object): pyd_dict['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']] # logger.debug(f"Equipment: {self.sub['equipment']}") try: - check = len(self.sub['equipment']) == 0 + check = bool(self.sub['equipment']) except TypeError: - check = True + check = False if check: - pyd_dict['equipment'] = None + pyd_dict['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']] else: - pyd_dict['equipment'] = self.sub['equipment'] + pyd_dict['equipment'] = None + try: + check = bool(self.sub['tips']) + except TypeError: + check = False + if check: + pyd_dict['tips'] = [PydTips(**tips) for tips in self.sub['tips']] + else: + pyd_dict['tips'] = None psm = PydSubmission(filepath=self.filepath, **pyd_dict) return psm @@ -535,6 +548,7 @@ class SampleParser(object): samples = remove_key_from_list_of_dicts(samples, "id") return sorted(samples, key=lambda k: (k['row'], k['column'])) + class EquipmentParser(object): def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None: @@ -567,15 +581,74 @@ class EquipmentParser(object): # logger.debug(f"Using equipment regex: {regex} on {input}") try: return regex.search(input).group().strip("-") - except AttributeError: + except AttributeError as e: + logger.error(f"Error getting asset number for {input}: {e}") return input - def parse_equipment(self) -> List[PydEquipment]: + def parse_equipment(self) -> List[dict]: """ Scrapes equipment from xl sheet Returns: - List[PydEquipment]: list of equipment + List[dict]: list of equipment + """ + logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}") + output = [] + # logger.debug(f"Sheets: {sheets}") + for sheet in self.xl.sheetnames: + ws = self.xl[sheet] + try: + relevant = {k:v for k,v in self.map.items() if v['sheet'] == sheet} + except (TypeError, KeyError) as e: + logger.error(f"Error creating relevant equipment list: {e}") + continue + logger.debug(f"Relevant equipment: {pformat(relevant)}") + previous_asset = "" + for k, v in relevant.items(): + logger.debug(f"Checking: {v}") + asset = ws.cell(v['name']['row'], v['name']['column']).value + if not check_not_nan(asset): + asset = previous_asset + else: + previous_asset = asset + asset = self.get_asset_number(input=asset) + logger.debug(f"asset: {asset}") + eq = Equipment.query(name=asset) + process = ws.cell(row=v['process']['row'], column=v['process']['column']).value + try: + output.append( + dict(name=eq.name, processes=[process], role=k, asset_number=eq.asset_number, + nickname=eq.nickname)) + except AttributeError: + logger.error(f"Unable to add {eq} to list.") + logger.debug(f"Here is the output so far: {pformat(output)}") + return output + + +class TipParser(object): + + def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None: + if isinstance(submission_type, str): + submission_type = SubmissionType.query(name=submission_type) + self.submission_type = submission_type + self.xl = xl + self.map = self.fetch_tip_map() + + def fetch_tip_map(self) -> List[dict]: + """ + Gets the map of equipment locations in the submission type's spreadsheet + + Returns: + List[dict]: List of locations + """ + return self.submission_type.construct_tips_map() + + def parse_tips(self) -> List[dict]: + """ + Scrapes equipment from xl sheet + + Returns: + List[dict]: list of equipment """ # logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}") output = [] @@ -583,29 +656,34 @@ class EquipmentParser(object): for sheet in self.xl.sheetnames: ws = self.xl[sheet] try: - relevant = [item for item in self.map if item['sheet'] == sheet] - except (TypeError, KeyError): + relevant = {k: v for k, v in self.map.items() if v['sheet'] == sheet} + except (TypeError, KeyError) as e: + logger.error(f"Error creating relevant equipment list: {e}") continue - # logger.debug(f"Relevant equipment: {pformat(relevant)}") + logger.debug(f"Relevant equipment: {pformat(relevant)}") previous_asset = "" - for equipment in relevant: - asset = ws.cell(equipment['name']['row'], equipment['name']['column']) + for k, v in relevant.items(): + asset = ws.cell(v['name']['row'], v['name']['column']).value + if "lot" in v.keys(): + lot = ws.cell(v['lot']['row'], v['lot']['column']).value + else: + lot = None if not check_not_nan(asset): asset = previous_asset else: previous_asset = asset - asset = self.get_asset_number(input=asset) - eq = Equipment.query(asset_number=asset) - process = ws.cell(row=equipment['process']['row'], column=equipment['process']['column']) + logger.debug(f"asset: {asset}") + eq = Tips.query(lot=lot, name=asset, limit=1) + # process = ws.cell(row=v['process']['row'], column=v['process']['column']).value try: output.append( - dict(name=eq.name, processes=[process], role=equipment['role'], asset_number=asset, - nickname=eq.nickname)) + dict(name=eq.name, role=k, lot=lot)) except AttributeError: - logger.error(f"Unable to add {eq} to PydEquipment list.") - # logger.debug(f"Here is the output so far: {pformat(output)}") + logger.error(f"Unable to add {eq} to PydTips list.") + logger.debug(f"Here is the output so far: {pformat(output)}") return output + class PCRParser(object): """Object to pull data from Design and Analysis PCR export file.""" diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index f9cafb9..1dd5927 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -179,4 +179,4 @@ class RSLNamer(object): from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ - PydEquipment, PydEquipmentRole + PydEquipment, PydEquipmentRole, PydTips diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 394b786..7568ff4 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -245,7 +245,10 @@ class PydSample(BaseModel, extra='allow'): instance.__setattr__(key, value) out_associations = [] if submission is not None: - assoc_type = self.sample_type.replace("Sample", "").strip() + if isinstance(submission, str): + submission = BasicSubmission.query(rsl_plate_num=submission) + assoc_type = submission.submission_type_name + # assoc_type = self.sample_type.replace("Sample", "").strip() for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank): # logger.debug(f"Looking up association with identity: ({submission.submission_type_name} Association)") # logger.debug(f"Looking up association with identity: ({assoc_type} Association)") @@ -254,7 +257,7 @@ class PydSample(BaseModel, extra='allow'): sample=instance, row=row, column=column, id=aid, submission_rank=submission_rank) - # logger.debug(f"Using submission_sample_association: {association}") + logger.debug(f"Using submission_sample_association: {association}") try: # instance.sample_submission_associations.append(association) out_associations.append(association) @@ -272,12 +275,24 @@ class PydSample(BaseModel, extra='allow'): return {k: getattr(self, k) for k in fields} +class PydTips(BaseModel): + name: str + lot: str|None = Field(default=None) + role: str + + def to_sql(self, submission:BasicSubmission): + tips = Tips.query(name=self.name, lot=self.lot, limit=1) + assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role) + return assoc + + class PydEquipment(BaseModel, extra='ignore'): asset_number: str name: str nickname: str | None processes: List[str] | None role: str | None + tips: List[PydTips]|None = Field(default=None) @field_validator('processes', mode='before') @classmethod @@ -703,18 +718,23 @@ class PydSubmission(BaseModel, extra='allow'): instance.submission_sample_associations.append(assoc) case "equipment": # logger.debug(f"Equipment: {pformat(self.equipment)}") - try: + for equip in self.equipment: if equip is None: continue - except UnboundLocalError: - continue - for equip in self.equipment: equip, association = equip.toSQL(submission=instance) if association is not None: - association.save() + # association.save() # logger.debug( # f"Equipment association SQL object to be added to submission: {association.__dict__}") instance.submission_equipment_associations.append(association) + case "tips": + for tips in self.tips: + if tips is None: + continue + association = tips.to_sql() + if association is not None: + # association.save() + instance.submission_tips_associations.append(association) case item if item in instance.jsons(): # logger.debug(f"{item} is a json.") try: @@ -949,3 +969,4 @@ class PydEquipmentRole(BaseModel): """ from frontend.widgets.equipment_usage import RoleComboBox return RoleComboBox(parent=parent, role=self, used=used) + diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index d793d4e..cccab99 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -1,9 +1,11 @@ +import sys +from pprint import pformat from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, - QLabel, QWidget, QHBoxLayout, - QVBoxLayout, QDialogButtonBox) -from backend.db.models import Equipment, BasicSubmission -from backend.validators.pydant import PydEquipment, PydEquipmentRole +from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, + QLabel, QWidget, QHBoxLayout, + QVBoxLayout, QDialogButtonBox, QGridLayout) +from backend.db.models import Equipment, BasicSubmission, Process +from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips import logging from typing import List @@ -56,7 +58,8 @@ class EquipmentUsage(QDialog): output.append(widget.parse_form()) case _: pass - return [item for item in output if item != None] + logger.debug(f"parsed output of Equsage form: {pformat(output)}") + return [item for item in output if item is not None] class LabelRow(QWidget): @@ -66,7 +69,7 @@ class EquipmentUsage(QDialog): self.check = QCheckBox() self.layout.addWidget(self.check) self.check.stateChanged.connect(self.check_all) - for item in ["Role", "Equipment", "Process"]: + for item in ["Role", "Equipment", "Process", "Tips"]: l = QLabel(item) l.setMaximumWidth(200) l.setMinimumWidth(200) @@ -84,7 +87,8 @@ class RoleComboBox(QWidget): def __init__(self, parent, role:PydEquipmentRole, used:list) -> None: super().__init__(parent) - self.layout = QHBoxLayout() + # self.layout = QHBoxLayout() + self.layout = QGridLayout() self.role = role self.check = QCheckBox() if role.name in used: @@ -99,15 +103,20 @@ class RoleComboBox(QWidget): self.process = QComboBox() self.process.setMaximumWidth(200) self.process.setMinimumWidth(200) - self.process.setEditable(True) - self.layout.addWidget(self.check) + self.process.setEditable(False) + self.process.currentTextChanged.connect(self.update_tips) + # self.tips = QComboBox() + # self.tips.setMaximumWidth(200) + # self.tips.setMinimumWidth(200) + # self.tips.setEditable(True) + self.layout.addWidget(self.check,0,0) label = QLabel(f"{role.name}:") label.setMinimumWidth(200) label.setMaximumWidth(200) label.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.layout.addWidget(label) - self.layout.addWidget(self.box) - self.layout.addWidget(self.process) + self.layout.addWidget(label,0,1) + self.layout.addWidget(self.box,0,2) + self.layout.addWidget(self.process,0,3) self.setLayout(self.layout) def update_processes(self): @@ -121,6 +130,28 @@ class RoleComboBox(QWidget): self.process.clear() self.process.addItems([item for item in equip2.processes if item in self.role.processes]) + def update_tips(self): + + process = self.process.currentText() + logger.debug(f"Checking process: {process}") + process = Process.query(name=process) + if process.tip_roles: + for iii, tip_role in enumerate(process.tip_roles): + widget = QComboBox() + tip_choices = [item.name for item in tip_role.instances] + widget.setEditable(False) + widget.addItems(tip_choices) + # logger.debug(f"Tiprole: {tip_role.__dict__}") + widget.setObjectName(f"tips_{tip_role.name}") + widget.setMinimumWidth(100) + widget.setMaximumWidth(100) + self.layout.addWidget(widget, iii, 4) + else: + widget = QLabel("") + widget.setMinimumWidth(100) + widget.setMaximumWidth(100) + self.layout.addWidget(widget,0,4) + def parse_form(self) -> PydEquipment|None: """ Creates PydEquipment for values in form @@ -129,8 +160,17 @@ 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("_")) for item in self.findChildren(QComboBox) if item.objectName().startswith("tips")] + logger.debug(tips) try: - return PydEquipment(name=eq.name, processes=[self.process.currentText()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname) + return PydEquipment( + name=eq.name, + processes=[self.process.currentText()], + role=self.role.name, + asset_number=eq.asset_number, + nickname=eq.nickname, + tips=tips + ) except Exception as e: logger.error(f"Could create PydEquipment due to: {e}") \ No newline at end of file diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicsubmission_details.html index cc87dfb..5e55af3 100644 --- a/src/submissions/templates/basicsubmission_details.html +++ b/src/submissions/templates/basicsubmission_details.html @@ -55,6 +55,12 @@     {{ item['role'] }}: {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '
        ') }}
{% endfor %}

{% endif %} + {% if sub['tips'] %} +

Tips:

+

{% for item in sub['tips'] %} +     {{ item['role'] }}: {{ item['name'] }} ({{ item['lot'] }})
+ {% endfor %}

+ {% endif %} {% if sub['samples'] %}

Samples:

{% for item in sub['samples'] %}