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'] %}