Tips parser complete.

This commit is contained in:
lwark
2024-06-13 09:11:53 -05:00
parent 78c92cd31f
commit e0e3080af0
7 changed files with 311 additions and 66 deletions

View File

@@ -8,13 +8,12 @@ from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date from datetime import date
import logging, re import logging, re
from tools import check_authorization, setup_lookup, Report, Result 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 pandas import ExcelFile
from pathlib import Path from pathlib import Path
from . import Base, BaseClass, Organization from . import Base, BaseClass, Organization
from io import BytesIO from io import BytesIO
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
# logger.debug("Table for ReagentType/Reagent relations") # logger.debug("Table for ReagentType/Reagent relations")
@@ -79,14 +78,6 @@ tiproles_tips = Table(
extend_existing=True 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 = Table(
"_process_tiprole", "_process_tiprole",
Base.metadata, Base.metadata,
@@ -95,6 +86,15 @@ process_tiprole = Table(
extend_existing=True 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): class KitType(BaseClass):
""" """
Base of kits used in submission processing Base of kits used in submission processing
@@ -133,7 +133,8 @@ class KitType(BaseClass):
""" """
return f"<KitType({self.name})>" return f"<KitType({self.name})>"
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. Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation.
@@ -609,6 +610,12 @@ class SubmissionType(BaseClass):
cascade="all, delete-orphan" cascade="all, delete-orphan"
) #: triple association of KitTypes, ReagentTypes, SubmissionTypes ) #: triple association of KitTypes, ReagentTypes, SubmissionTypes
submissiontype_tiprole_associations = relationship(
"SubmissionTypeTipRoleAssociation",
back_populates="submission_type",
cascade="all, delete-orphan"
)
def __repr__(self) -> str: def __repr__(self) -> str:
""" """
Returns: Returns:
@@ -693,6 +700,15 @@ class SubmissionType(BaseClass):
output[item.equipment_role.name] = emap output[item.equipment_role.name] = emap
return output 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']: def get_equipment(self, extraction_kit: str | KitType | None = None) -> List['PydEquipmentRole']:
""" """
Returns PydEquipmentRole of all equipment associated with this SubmissionType Returns PydEquipmentRole of all equipment associated with this SubmissionType
@@ -1071,6 +1087,8 @@ class Equipment(BaseClass):
secondary=equipmentroles_equipment) #: relation to EquipmentRoles secondary=equipmentroles_equipment) #: relation to EquipmentRoles
processes = relationship("Process", back_populates="equipment", processes = relationship("Process", back_populates="equipment",
secondary=equipment_processes) #: relation to Processes secondary=equipment_processes) #: relation to Processes
tips = relationship("Tips", back_populates="equipment",
secondary=equipment_tips) #: relation to Processes
equipment_submission_associations = relationship( equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation", "SubmissionEquipmentAssociation",
back_populates="equipment", back_populates="equipment",
@@ -1467,7 +1485,7 @@ class Process(BaseClass):
backref='process') #: relation to SubmissionEquipmentAssociation backref='process') #: relation to SubmissionEquipmentAssociation
kit_types = relationship("KitType", back_populates='processes', kit_types = relationship("KitType", back_populates='processes',
secondary=kittypes_processes) #: relation to KitType 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 secondary=process_tiprole) #: relation to KitType
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -1502,19 +1520,25 @@ class Process(BaseClass):
class TipRole(BaseClass): class TipRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type name = Column(String(64)) #: name of reagent type
instances = relationship("Tips", back_populates="role", instances = relationship("Tips", back_populates="role",
secondary=tiproles_tips) #: concrete instances of this reagent type secondary=tiproles_tips) #: concrete instances of this reagent type
processes = relationship("Process", back_populates="tip_roles", secondary=process_tiprole) 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): def __repr__(self):
return f"<TipRole({self.name})>" return f"<TipRole({self.name})>"
class Tips(BaseClass):
class Tips(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("TipRole", back_populates="instances", role = relationship("TipRole", back_populates="instances",
secondary=tiproles_tips) #: joined parent reagent type 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="fk_tip_role_id")) #: id of parent reagent type
name = Column(String(64)) #: tip common name name = Column(String(64)) #: tip common name
lot = Column(String(64)) #: lot number of tips lot = Column(String(64)) #: lot number of tips
submissions = relationship("BasicSubmission", back_populates="tips", equipment = relationship("Equipment", back_populates="tips",
secondary=submissions_tips) #: associated submission 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): def __repr__(self):
return f"<Tips({self.name})>" return f"<Tips({self.name})>"
@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)

View File

@@ -10,7 +10,7 @@ from zipfile import ZipFile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from operator import attrgetter, itemgetter from operator import attrgetter, itemgetter
from pprint import pformat 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 import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
@@ -96,6 +96,14 @@ class BasicSubmission(BaseClass):
equipment = association_proxy("submission_equipment_associations", equipment = association_proxy("submission_equipment_associations",
"equipment") #: Association proxy to SubmissionEquipmentAssociation.equipment "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. # NOTE: Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "Basic Submission", "polymorphic_identity": "Basic Submission",
@@ -248,7 +256,6 @@ class BasicSubmission(BaseClass):
ext_info = self.extraction_info ext_info = self.extraction_info
except TypeError: except TypeError:
ext_info = None ext_info = None
output = { output = {
"id": self.id, "id": self.id,
"plate_number": self.rsl_plate_num, "plate_number": self.rsl_plate_num,
@@ -282,16 +289,24 @@ class BasicSubmission(BaseClass):
# logger.debug("Running equipment") # logger.debug("Running equipment")
try: try:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations] equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if len(equipment) == 0: if not equipment:
equipment = None equipment = None
except Exception as e: except Exception as e:
logger.error(f"Error setting equipment: {e}") logger.error(f"Error setting equipment: {e}")
equipment = None 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 cost_centre = self.cost_centre
else: else:
reagents = None reagents = None
samples = None samples = None
equipment = None equipment = None
tips = None
cost_centre = None cost_centre = None
# logger.debug("Getting comments") # logger.debug("Getting comments")
try: try:
@@ -315,6 +330,7 @@ class BasicSubmission(BaseClass):
output["extraction_info"] = ext_info output["extraction_info"] = ext_info
output["comment"] = comments output["comment"] = comments
output["equipment"] = equipment output["equipment"] = equipment
output["tips"] = tips
output["cost_centre"] = cost_centre output["cost_centre"] = cost_centre
output["signed_by"] = self.signed_by output["signed_by"] = self.signed_by
# logger.debug(f"Setting contact to: {contact} of type: {type(contact)}") # 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', excluded = ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents',
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls', 'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls',
'source_plates', 'pcr_technician', 'ext_technician', 'artic_technician', 'cost_centre', '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: for item in excluded:
try: try:
df = df.drop(item, axis=1) df = df.drop(item, axis=1)
@@ -1110,6 +1126,12 @@ class BasicSubmission(BaseClass):
_, assoc = equip.toSQL(submission=self) _, assoc = equip.toSQL(submission=self)
# logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}") # logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
assoc.save() 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: else:
pass pass

View File

@@ -10,7 +10,7 @@ import pandas as pd
from openpyxl import load_workbook, Workbook from openpyxl import load_workbook, Workbook
from pathlib import Path from pathlib import Path
from backend.db.models import * 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 import logging, re
from collections import OrderedDict from collections import OrderedDict
from datetime import date from datetime import date
@@ -57,6 +57,7 @@ class SheetParser(object):
self.parse_reagents() self.parse_reagents()
self.parse_samples() self.parse_samples()
self.parse_equipment() self.parse_equipment()
self.parse_tips()
self.finalize_parse() self.finalize_parse()
# logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}") # 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) parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type)
self.sub['equipment'] = parser.parse_equipment() 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): def import_kit_validation_check(self):
""" """
Enforce that the parser has an extraction kit 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']] pyd_dict['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']]
# logger.debug(f"Equipment: {self.sub['equipment']}") # logger.debug(f"Equipment: {self.sub['equipment']}")
try: try:
check = len(self.sub['equipment']) == 0 check = bool(self.sub['equipment'])
except TypeError: except TypeError:
check = True check = False
if check: if check:
pyd_dict['equipment'] = None pyd_dict['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']]
else: 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) psm = PydSubmission(filepath=self.filepath, **pyd_dict)
return psm return psm
@@ -535,6 +548,7 @@ class SampleParser(object):
samples = remove_key_from_list_of_dicts(samples, "id") samples = remove_key_from_list_of_dicts(samples, "id")
return sorted(samples, key=lambda k: (k['row'], k['column'])) return sorted(samples, key=lambda k: (k['row'], k['column']))
class EquipmentParser(object): class EquipmentParser(object):
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None: 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}") # logger.debug(f"Using equipment regex: {regex} on {input}")
try: try:
return regex.search(input).group().strip("-") return regex.search(input).group().strip("-")
except AttributeError: except AttributeError as e:
logger.error(f"Error getting asset number for {input}: {e}")
return input return input
def parse_equipment(self) -> List[PydEquipment]: def parse_equipment(self) -> List[dict]:
""" """
Scrapes equipment from xl sheet Scrapes equipment from xl sheet
Returns: 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__)}") # logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}")
output = [] output = []
@@ -583,29 +656,34 @@ class EquipmentParser(object):
for sheet in self.xl.sheetnames: for sheet in self.xl.sheetnames:
ws = self.xl[sheet] ws = self.xl[sheet]
try: try:
relevant = [item for item in self.map if item['sheet'] == sheet] relevant = {k: v for k, v in self.map.items() if v['sheet'] == sheet}
except (TypeError, KeyError): except (TypeError, KeyError) as e:
logger.error(f"Error creating relevant equipment list: {e}")
continue continue
# logger.debug(f"Relevant equipment: {pformat(relevant)}") logger.debug(f"Relevant equipment: {pformat(relevant)}")
previous_asset = "" previous_asset = ""
for equipment in relevant: for k, v in relevant.items():
asset = ws.cell(equipment['name']['row'], equipment['name']['column']) 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): if not check_not_nan(asset):
asset = previous_asset asset = previous_asset
else: else:
previous_asset = asset previous_asset = asset
asset = self.get_asset_number(input=asset) logger.debug(f"asset: {asset}")
eq = Equipment.query(asset_number=asset) eq = Tips.query(lot=lot, name=asset, limit=1)
process = ws.cell(row=equipment['process']['row'], column=equipment['process']['column']) # process = ws.cell(row=v['process']['row'], column=v['process']['column']).value
try: try:
output.append( output.append(
dict(name=eq.name, processes=[process], role=equipment['role'], asset_number=asset, dict(name=eq.name, role=k, lot=lot))
nickname=eq.nickname))
except AttributeError: except AttributeError:
logger.error(f"Unable to add {eq} to PydEquipment list.") logger.error(f"Unable to add {eq} to PydTips list.")
# logger.debug(f"Here is the output so far: {pformat(output)}") logger.debug(f"Here is the output so far: {pformat(output)}")
return output return output
class PCRParser(object): class PCRParser(object):
"""Object to pull data from Design and Analysis PCR export file.""" """Object to pull data from Design and Analysis PCR export file."""

View File

@@ -179,4 +179,4 @@ class RSLNamer(object):
from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
PydEquipment, PydEquipmentRole PydEquipment, PydEquipmentRole, PydTips

View File

@@ -245,7 +245,10 @@ class PydSample(BaseModel, extra='allow'):
instance.__setattr__(key, value) instance.__setattr__(key, value)
out_associations = [] out_associations = []
if submission is not None: 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): 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: ({submission.submission_type_name} Association)")
# logger.debug(f"Looking up association with identity: ({assoc_type} Association)") # logger.debug(f"Looking up association with identity: ({assoc_type} Association)")
@@ -254,7 +257,7 @@ class PydSample(BaseModel, extra='allow'):
sample=instance, sample=instance,
row=row, column=column, id=aid, row=row, column=column, id=aid,
submission_rank=submission_rank) submission_rank=submission_rank)
# logger.debug(f"Using submission_sample_association: {association}") logger.debug(f"Using submission_sample_association: {association}")
try: try:
# instance.sample_submission_associations.append(association) # instance.sample_submission_associations.append(association)
out_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} 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'): class PydEquipment(BaseModel, extra='ignore'):
asset_number: str asset_number: str
name: str name: str
nickname: str | None nickname: str | None
processes: List[str] | None processes: List[str] | None
role: str | None role: str | None
tips: List[PydTips]|None = Field(default=None)
@field_validator('processes', mode='before') @field_validator('processes', mode='before')
@classmethod @classmethod
@@ -703,18 +718,23 @@ class PydSubmission(BaseModel, extra='allow'):
instance.submission_sample_associations.append(assoc) instance.submission_sample_associations.append(assoc)
case "equipment": case "equipment":
# logger.debug(f"Equipment: {pformat(self.equipment)}") # logger.debug(f"Equipment: {pformat(self.equipment)}")
try: for equip in self.equipment:
if equip is None: if equip is None:
continue continue
except UnboundLocalError:
continue
for equip in self.equipment:
equip, association = equip.toSQL(submission=instance) equip, association = equip.toSQL(submission=instance)
if association is not None: if association is not None:
association.save() # association.save()
# logger.debug( # logger.debug(
# f"Equipment association SQL object to be added to submission: {association.__dict__}") # f"Equipment association SQL object to be added to submission: {association.__dict__}")
instance.submission_equipment_associations.append(association) 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(): case item if item in instance.jsons():
# logger.debug(f"{item} is a json.") # logger.debug(f"{item} is a json.")
try: try:
@@ -949,3 +969,4 @@ class PydEquipmentRole(BaseModel):
""" """
from frontend.widgets.equipment_usage import RoleComboBox from frontend.widgets.equipment_usage import RoleComboBox
return RoleComboBox(parent=parent, role=self, used=used) return RoleComboBox(parent=parent, role=self, used=used)

View File

@@ -1,9 +1,11 @@
import sys
from pprint import pformat
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QHBoxLayout, QLabel, QWidget, QHBoxLayout,
QVBoxLayout, QDialogButtonBox) QVBoxLayout, QDialogButtonBox, QGridLayout)
from backend.db.models import Equipment, BasicSubmission from backend.db.models import Equipment, BasicSubmission, Process
from backend.validators.pydant import PydEquipment, PydEquipmentRole from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging import logging
from typing import List from typing import List
@@ -56,7 +58,8 @@ class EquipmentUsage(QDialog):
output.append(widget.parse_form()) output.append(widget.parse_form())
case _: case _:
pass 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): class LabelRow(QWidget):
@@ -66,7 +69,7 @@ class EquipmentUsage(QDialog):
self.check = QCheckBox() self.check = QCheckBox()
self.layout.addWidget(self.check) self.layout.addWidget(self.check)
self.check.stateChanged.connect(self.check_all) self.check.stateChanged.connect(self.check_all)
for item in ["Role", "Equipment", "Process"]: for item in ["Role", "Equipment", "Process", "Tips"]:
l = QLabel(item) l = QLabel(item)
l.setMaximumWidth(200) l.setMaximumWidth(200)
l.setMinimumWidth(200) l.setMinimumWidth(200)
@@ -84,7 +87,8 @@ class RoleComboBox(QWidget):
def __init__(self, parent, role:PydEquipmentRole, used:list) -> None: def __init__(self, parent, role:PydEquipmentRole, used:list) -> None:
super().__init__(parent) super().__init__(parent)
self.layout = QHBoxLayout() # self.layout = QHBoxLayout()
self.layout = QGridLayout()
self.role = role self.role = role
self.check = QCheckBox() self.check = QCheckBox()
if role.name in used: if role.name in used:
@@ -99,15 +103,20 @@ class RoleComboBox(QWidget):
self.process = QComboBox() self.process = QComboBox()
self.process.setMaximumWidth(200) self.process.setMaximumWidth(200)
self.process.setMinimumWidth(200) self.process.setMinimumWidth(200)
self.process.setEditable(True) self.process.setEditable(False)
self.layout.addWidget(self.check) 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 = QLabel(f"{role.name}:")
label.setMinimumWidth(200) label.setMinimumWidth(200)
label.setMaximumWidth(200) label.setMaximumWidth(200)
label.setAlignment(Qt.AlignmentFlag.AlignLeft) label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.layout.addWidget(label) self.layout.addWidget(label,0,1)
self.layout.addWidget(self.box) self.layout.addWidget(self.box,0,2)
self.layout.addWidget(self.process) self.layout.addWidget(self.process,0,3)
self.setLayout(self.layout) self.setLayout(self.layout)
def update_processes(self): def update_processes(self):
@@ -121,6 +130,28 @@ class RoleComboBox(QWidget):
self.process.clear() 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.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: def parse_form(self) -> PydEquipment|None:
""" """
Creates PydEquipment for values in form Creates PydEquipment for values in form
@@ -129,8 +160,17 @@ class RoleComboBox(QWidget):
PydEquipment|None: PydEquipment matching form PydEquipment|None: PydEquipment matching form
""" """
eq = Equipment.query(name=self.box.currentText()) 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: 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: except Exception as e:
logger.error(f"Could create PydEquipment due to: {e}") logger.error(f"Could create PydEquipment due to: {e}")

View File

@@ -55,6 +55,12 @@
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}<br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}<br>
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}
{% if sub['tips'] %}
<h3><u>Tips:</u></h3>
<p>{% for item in sub['tips'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['lot'] }})<br>
{% endfor %}</p>
{% endif %}
{% if sub['samples'] %} {% if sub['samples'] %}
<h3><u>Samples:</u></h3> <h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %} <p>{% for item in sub['samples'] %}