Tips parser complete.
This commit is contained in:
@@ -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"<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.
|
||||
|
||||
@@ -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"<TipRole({self.name})>"
|
||||
|
||||
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"<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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -179,4 +179,4 @@ class RSLNamer(object):
|
||||
|
||||
|
||||
from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
|
||||
PydEquipment, PydEquipmentRole
|
||||
PydEquipment, PydEquipmentRole, PydTips
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -55,6 +55,12 @@
|
||||
<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br> ') }}<br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['tips'] %}
|
||||
<h3><u>Tips:</u></h3>
|
||||
<p>{% for item in sub['tips'] %}
|
||||
<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['lot'] }})<br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['samples'] %}
|
||||
<h3><u>Samples:</u></h3>
|
||||
<p>{% for item in sub['samples'] %}
|
||||
|
||||
Reference in New Issue
Block a user