Midway through disaster of changing table names.
This commit is contained in:
@@ -18,9 +18,13 @@ class BaseClass(Base):
|
||||
Base (DeclarativeMeta): Declarative base for metadata.
|
||||
"""
|
||||
__abstract__ = True
|
||||
|
||||
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return f"_{cls.__name__.lower()}"
|
||||
|
||||
@declared_attr
|
||||
def __database_session__(cls):
|
||||
if not 'pytest' in sys.modules:
|
||||
@@ -44,6 +48,15 @@ class BaseClass(Base):
|
||||
else:
|
||||
from test_settings import ctx
|
||||
return ctx.backup_path
|
||||
|
||||
def save(self):
|
||||
logger.debug(f"Saving {self}")
|
||||
try:
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
except Exception as e:
|
||||
logger.critical(f"Problem saving object: {e}")
|
||||
self.__database_session__.rollback()
|
||||
|
||||
from .controls import *
|
||||
# import order must go: orgs, kit, subs due to circular import issues
|
||||
|
||||
@@ -18,7 +18,7 @@ class ControlType(BaseClass):
|
||||
"""
|
||||
Base class of a control archetype.
|
||||
"""
|
||||
__tablename__ = '_control_types'
|
||||
# __tablename__ = '_control_types'
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
|
||||
@@ -75,7 +75,7 @@ class Control(BaseClass):
|
||||
Base class of a control sample.
|
||||
"""
|
||||
|
||||
__tablename__ = '_control_samples'
|
||||
# __tablename__ = '_control_samples'
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type
|
||||
@@ -264,7 +264,4 @@ class Control(BaseClass):
|
||||
case _:
|
||||
pass
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def save(self):
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from typing import List
|
||||
from pandas import ExcelFile
|
||||
from pathlib import Path
|
||||
from . import Base, BaseClass, Organization
|
||||
from tools import Settings
|
||||
|
||||
logger = logging.getLogger(f'submissions.{__name__}')
|
||||
|
||||
@@ -32,11 +31,19 @@ equipmentroles_equipment = Table(
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
equipment_processes = Table(
|
||||
"_equipment_processes",
|
||||
Base.metadata,
|
||||
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
equipmentroles_processes = Table(
|
||||
"_equipmentroles_processes",
|
||||
Base.metadata,
|
||||
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||
Column("equipmentroles_id", INTEGER, ForeignKey("_equipment_roles.id")),
|
||||
Column("equipmentrole_id", INTEGER, ForeignKey("_equipment_roles.id")),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
@@ -48,16 +55,24 @@ submissiontypes_processes = Table(
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
kittypes_processes = Table(
|
||||
"_kittypes_processes",
|
||||
Base.metadata,
|
||||
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||
Column("kit_id", INTEGER, ForeignKey("_kits.id")),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
class KitType(BaseClass):
|
||||
"""
|
||||
Base of kits used in submission processing
|
||||
"""
|
||||
__tablename__ = "_kits"
|
||||
# __table_args__ = {'extend_existing': True}
|
||||
# __tablename__ = "_kits"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64), unique=True) #: name of kit
|
||||
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
|
||||
processes = relationship("Process", back_populates="kit_types", secondary=kittypes_processes)
|
||||
|
||||
kit_reagenttype_associations = relationship(
|
||||
"KitTypeReagentTypeAssociation",
|
||||
@@ -87,7 +102,7 @@ class KitType(BaseClass):
|
||||
|
||||
Args:
|
||||
required (bool, optional): If true only return required types. Defaults to False.
|
||||
submission_type (str | None, optional): Submission type to narrow results. Defaults to None.
|
||||
submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None.
|
||||
|
||||
Returns:
|
||||
list: List of reagent types
|
||||
@@ -109,12 +124,13 @@ class KitType(BaseClass):
|
||||
Creates map of locations in excel workbook for a SubmissionType
|
||||
|
||||
Args:
|
||||
use (str): Submissiontype.name
|
||||
use (str | SubmissionType): Submissiontype.name
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing information locations.
|
||||
"""
|
||||
map = {}
|
||||
# Account for submission_type variable type.
|
||||
match submission_type:
|
||||
case str():
|
||||
assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name==submission_type]
|
||||
@@ -125,7 +141,6 @@ class KitType(BaseClass):
|
||||
case _:
|
||||
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
|
||||
# Get all KitTypeReagentTypeAssociation for SubmissionType
|
||||
# assocs = [item for item in self.kit_reagenttype_associations if item.submission_type==submission_type]
|
||||
for assoc in assocs:
|
||||
try:
|
||||
map[assoc.reagent_type.name] = assoc.uses
|
||||
@@ -133,7 +148,6 @@ class KitType(BaseClass):
|
||||
continue
|
||||
# Get SubmissionType info map
|
||||
try:
|
||||
# st_assoc = [item for item in self.used_for if use == item.name][0]
|
||||
map['info'] = st_assoc.info_map
|
||||
except IndexError as e:
|
||||
map['info'] = {}
|
||||
@@ -152,7 +166,7 @@ class KitType(BaseClass):
|
||||
|
||||
Args:
|
||||
name (str, optional): Name of desired kit (returns single instance). Defaults to None.
|
||||
used_for (str | models.Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
|
||||
used_for (str | Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
|
||||
id (int | None, optional): Kit id in the database. Defaults to None.
|
||||
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
|
||||
@@ -190,17 +204,13 @@ class KitType(BaseClass):
|
||||
|
||||
@check_authorization
|
||||
def save(self, ctx:Settings):
|
||||
"""
|
||||
Add this instance to database and commit
|
||||
"""
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
super().save()
|
||||
|
||||
class ReagentType(BaseClass):
|
||||
"""
|
||||
Base of reagent type abstract
|
||||
"""
|
||||
__tablename__ = "_reagent_types"
|
||||
# __tablename__ = "_reagent_types"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64)) #: name of reagent type
|
||||
@@ -281,130 +291,16 @@ class ReagentType(BaseClass):
|
||||
def to_pydantic(self):
|
||||
from backend.validators.pydant import PydReagent
|
||||
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
|
||||
|
||||
# class KitTypeReagentTypeAssociation(BaseClass):
|
||||
# """
|
||||
# table containing reagenttype/kittype associations
|
||||
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||
# """
|
||||
# __tablename__ = "_reagenttypes_kittypes"
|
||||
|
||||
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
|
||||
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
|
||||
# submission_type_id = (Column(INTEGER), ForeignKey("_submission_types.id"), primary_key=True)
|
||||
# uses = Column(JSON) #: map to location on excel sheets of different submission types
|
||||
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
|
||||
# last_used = Column(String(32)) #: last used lot number of this type of reagent
|
||||
|
||||
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
|
||||
|
||||
# # reference to the "ReagentType" object
|
||||
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
|
||||
|
||||
# submission_type = relationship(SubmissionType, back_populates="submissiontype_kit_rt_associations")
|
||||
|
||||
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
|
||||
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
|
||||
# self.kit_type = kit_type
|
||||
# self.reagent_type = reagent_type
|
||||
# self.uses = uses
|
||||
# self.required = required
|
||||
|
||||
# def __repr__(self) -> str:
|
||||
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
|
||||
|
||||
# @validates('required')
|
||||
# def validate_age(self, key, value):
|
||||
# """
|
||||
# Ensures only 1 & 0 used in 'required'
|
||||
|
||||
# Args:
|
||||
# key (str): name of attribute
|
||||
# value (_type_): value of attribute
|
||||
|
||||
# Raises:
|
||||
# ValueError: Raised if bad value given
|
||||
|
||||
# Returns:
|
||||
# _type_: value
|
||||
# """
|
||||
# if not 0 <= value < 2:
|
||||
# raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
||||
# return value
|
||||
|
||||
# @validates('reagenttype')
|
||||
# def validate_reagenttype(self, key, value):
|
||||
# """
|
||||
# Ensures reagenttype is an actual ReagentType
|
||||
|
||||
# Args:
|
||||
# key (str)): name of attribute
|
||||
# value (_type_): value of attribute
|
||||
|
||||
# Raises:
|
||||
# ValueError: raised if reagenttype is not a ReagentType
|
||||
|
||||
# Returns:
|
||||
# _type_: ReagentType
|
||||
# """
|
||||
# if not isinstance(value, ReagentType):
|
||||
# raise ValueError(f'{value} is not a reagenttype')
|
||||
# return value
|
||||
|
||||
# @classmethod
|
||||
# @setup_lookup
|
||||
# def query(cls,
|
||||
# kit_type:KitType|str|None=None,
|
||||
# reagent_type:ReagentType|str|None=None,
|
||||
# limit:int=0
|
||||
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
|
||||
# """
|
||||
# Lookup junction of ReagentType and KitType
|
||||
|
||||
# Args:
|
||||
# kit_type (models.KitType | str | None): KitType of interest.
|
||||
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
|
||||
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
|
||||
# Returns:
|
||||
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
|
||||
# """
|
||||
# query: Query = cls.__database_session__.query(cls)
|
||||
# match kit_type:
|
||||
# case KitType():
|
||||
# query = query.filter(cls.kit_type==kit_type)
|
||||
# case str():
|
||||
# query = query.join(KitType).filter(KitType.name==kit_type)
|
||||
# case _:
|
||||
# pass
|
||||
# match reagent_type:
|
||||
# case ReagentType():
|
||||
# query = query.filter(cls.reagent_type==reagent_type)
|
||||
# case str():
|
||||
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
|
||||
# case _:
|
||||
# pass
|
||||
# if kit_type != None and reagent_type != None:
|
||||
# limit = 1
|
||||
# return query_return(query=query, limit=limit)
|
||||
|
||||
# def save(self) -> Report:
|
||||
# """
|
||||
# Adds this instance to the database and commits.
|
||||
|
||||
# Returns:
|
||||
# Report: Result of save action
|
||||
# """
|
||||
# report = Report()
|
||||
# self.__database_session__.add(self)
|
||||
# self.__database_session__.commit()
|
||||
# return report
|
||||
@check_authorization
|
||||
def save(self, ctx:Settings):
|
||||
super().save()
|
||||
|
||||
class Reagent(BaseClass):
|
||||
"""
|
||||
Concrete reagent instance
|
||||
"""
|
||||
__tablename__ = "_reagents"
|
||||
# __tablename__ = "_reagents"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type
|
||||
@@ -412,7 +308,6 @@ class Reagent(BaseClass):
|
||||
name = Column(String(64)) #: reagent name
|
||||
lot = Column(String(64)) #: lot number of reagent
|
||||
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
|
||||
# submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in
|
||||
|
||||
reagent_submission_associations = relationship(
|
||||
"SubmissionReagentAssociation",
|
||||
@@ -497,6 +392,7 @@ class Reagent(BaseClass):
|
||||
def query(cls,
|
||||
reagent_type:str|ReagentType|None=None,
|
||||
lot_number:str|None=None,
|
||||
name:str|None=None,
|
||||
limit:int=0
|
||||
) -> Reagent|List[Reagent]:
|
||||
"""
|
||||
@@ -505,6 +401,7 @@ class Reagent(BaseClass):
|
||||
Args:
|
||||
reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None.
|
||||
lot_number (str | None, optional): Reagent lot number. Defaults to None.
|
||||
name (str | None, optional): Reagent name. Defaults to None.
|
||||
limit (int, optional): limit of results returned. Defaults to 0.
|
||||
|
||||
Returns:
|
||||
@@ -521,6 +418,12 @@ class Reagent(BaseClass):
|
||||
query = query.filter(cls.type.contains(reagent_type))
|
||||
case _:
|
||||
pass
|
||||
match name:
|
||||
case str():
|
||||
logger.debug(f"Looking up reagent by name: {name}")
|
||||
query = query.filter(cls.name==name)
|
||||
case _:
|
||||
pass
|
||||
match lot_number:
|
||||
case str():
|
||||
logger.debug(f"Looking up reagent by lot number: {lot_number}")
|
||||
@@ -530,19 +433,12 @@ class Reagent(BaseClass):
|
||||
case _:
|
||||
pass
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Add this instance to the database and commit
|
||||
"""
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
|
||||
class Discount(BaseClass):
|
||||
"""
|
||||
Relationship table for client labs for certain kits.
|
||||
"""
|
||||
__tablename__ = "_discounts"
|
||||
# __tablename__ = "_discounts"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
kit = relationship("KitType") #: joined parent reagent type
|
||||
@@ -604,17 +500,20 @@ class Discount(BaseClass):
|
||||
pass
|
||||
return query.all()
|
||||
|
||||
@check_authorization
|
||||
def save(self, ctx:Settings):
|
||||
super().save()
|
||||
|
||||
class SubmissionType(BaseClass):
|
||||
"""
|
||||
Abstract of types of submissions.
|
||||
"""
|
||||
__tablename__ = "_submission_types"
|
||||
# __tablename__ = "_submission_types"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(128), unique=True) #: name of submission type
|
||||
info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type.
|
||||
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
|
||||
# regex = Column(String(512))
|
||||
template_file = Column(BLOB) #: Blank form for this type stored as binary.
|
||||
processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes)
|
||||
|
||||
@@ -664,20 +563,25 @@ class SubmissionType(BaseClass):
|
||||
output = []
|
||||
for item in self.submissiontype_equipmentrole_associations:
|
||||
map = item.uses
|
||||
map['role'] = item.equipment_role.name
|
||||
if map == None:
|
||||
map = {}
|
||||
try:
|
||||
map['role'] = item.equipment_role.name
|
||||
except TypeError:
|
||||
pass
|
||||
output.append(map)
|
||||
return output
|
||||
# return [item.uses for item in self.submissiontype_equipmentrole_associations]
|
||||
|
||||
def get_equipment(self) -> List['PydEquipmentRole']:
|
||||
return [item.to_pydantic(submission_type=self) for item in self.equipment]
|
||||
def get_equipment(self, extraction_kit:str|KitType|None=None) -> List['PydEquipmentRole']:
|
||||
return [item.to_pydantic(submission_type=self, extraction_kit=extraction_kit) for item in self.equipment]
|
||||
|
||||
def get_processes_for_role(self, equipment_role:str|EquipmentRole):
|
||||
def get_processes_for_role(self, equipment_role:str|EquipmentRole, kit:str|KitType|None=None):
|
||||
match equipment_role:
|
||||
case str():
|
||||
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
|
||||
relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
|
||||
case EquipmentRole():
|
||||
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
|
||||
relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
|
||||
case _:
|
||||
raise TypeError(f"Type {type(equipment_role)} is not allowed")
|
||||
return list(set([item for items in relevant for item in items if item != None ]))
|
||||
@@ -728,7 +632,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
||||
"""
|
||||
Abstract of relationship between kits and their submission type.
|
||||
"""
|
||||
__tablename__ = "_submissiontypes_kittypes"
|
||||
# __tablename__ = "_submissiontypes_kittypes"
|
||||
|
||||
submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of joined submission type
|
||||
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of joined kit
|
||||
@@ -801,7 +705,7 @@ class KitTypeReagentTypeAssociation(BaseClass):
|
||||
table containing reagenttype/kittype associations
|
||||
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||
"""
|
||||
__tablename__ = "_reagenttypes_kittypes"
|
||||
# __tablename__ = "_reagenttypes_kittypes"
|
||||
|
||||
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
|
||||
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
|
||||
@@ -902,22 +806,9 @@ class KitTypeReagentTypeAssociation(BaseClass):
|
||||
limit = 1
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def save(self) -> Report:
|
||||
"""
|
||||
Adds this instance to the database and commits.
|
||||
|
||||
Returns:
|
||||
Report: Result of save action
|
||||
"""
|
||||
report = Report()
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
return report
|
||||
|
||||
|
||||
class SubmissionReagentAssociation(BaseClass):
|
||||
|
||||
__tablename__ = "_reagents_submissions"
|
||||
# __tablename__ = "_reagents_submissions"
|
||||
|
||||
reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample
|
||||
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True)
|
||||
@@ -961,8 +852,6 @@ class SubmissionReagentAssociation(BaseClass):
|
||||
# logger.debug(f"Filtering query with reagent: {reagent}")
|
||||
reagent = Reagent.query(lot_number=reagent)
|
||||
query = query.filter(cls.reagent==reagent)
|
||||
# logger.debug([item.reagent.lot for item in query.all()])
|
||||
# query = query.join(Reagent).filter(Reagent.lot==reagent)
|
||||
case _:
|
||||
pass
|
||||
# logger.debug(f"Result of query after reagent: {query.all()}")
|
||||
@@ -976,7 +865,6 @@ class SubmissionReagentAssociation(BaseClass):
|
||||
case _:
|
||||
pass
|
||||
# logger.debug(f"Result of query after submission: {query.all()}")
|
||||
# limit = query.count()
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def to_sub_dict(self, extraction_kit):
|
||||
@@ -989,13 +877,14 @@ class Equipment(BaseClass):
|
||||
# Currently abstract until ready to implement
|
||||
# __abstract__ = True
|
||||
|
||||
__tablename__ = "_equipment"
|
||||
# __tablename__ = "_equipment"
|
||||
|
||||
id = Column(INTEGER, primary_key=True)
|
||||
name = Column(String(64))
|
||||
nickname = Column(String(64))
|
||||
asset_number = Column(String(16))
|
||||
roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment)
|
||||
processes = relationship("Process", back_populates="equipment", secondary=equipment_processes)
|
||||
|
||||
equipment_submission_associations = relationship(
|
||||
"SubmissionEquipmentAssociation",
|
||||
@@ -1008,10 +897,30 @@ class Equipment(BaseClass):
|
||||
def __repr__(self):
|
||||
return f"<Equipment({self.name})>"
|
||||
|
||||
def get_processes(self, submission_type:SubmissionType):
|
||||
processes = [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
|
||||
def to_dict(self, processes:bool=False):
|
||||
if not processes:
|
||||
return {k:v for k,v in self.__dict__.items() if k != 'processes'}
|
||||
else:
|
||||
return {k:v for k,v in self.__dict__.items()}
|
||||
|
||||
def get_processes(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
|
||||
processes = [process for process in self.processes if submission_type in process.submission_types]
|
||||
match extraction_kit:
|
||||
case str():
|
||||
processes = [process for process in processes if extraction_kit in [kit.name for kit in process.kit_types]]
|
||||
case KitType():
|
||||
processes = [process for process in processes if extraction_kit in process.kit_types]
|
||||
case _:
|
||||
pass
|
||||
processes = [process.name for process in processes]
|
||||
# try:
|
||||
assert all([isinstance(process, str) for process in processes])
|
||||
# except AssertionError as e:
|
||||
# logger.error(processes)
|
||||
# raise e
|
||||
if len(processes) == 0:
|
||||
processes = ['']
|
||||
# logger.debug(f"Processes: {processes}")
|
||||
return processes
|
||||
|
||||
@classmethod
|
||||
@@ -1043,10 +952,10 @@ class Equipment(BaseClass):
|
||||
pass
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def to_pydantic(self, submission_type:SubmissionType):
|
||||
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
|
||||
from backend.validators.pydant import PydEquipment
|
||||
# return PydEquipment(process=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
|
||||
return PydEquipment(process=None, role=None, **self.__dict__)
|
||||
return PydEquipment(processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=None, **self.to_dict(processes=False))
|
||||
# return PydEquipment(process=None, role=None, **self.__dict__)
|
||||
|
||||
def save(self):
|
||||
self.__database_session__.add(self)
|
||||
@@ -1064,12 +973,12 @@ class Equipment(BaseClass):
|
||||
|
||||
class EquipmentRole(BaseClass):
|
||||
|
||||
__tablename__ = "_equipment_roles"
|
||||
# __tablename__ = "_equipment_roles"
|
||||
|
||||
id = Column(INTEGER, primary_key=True)
|
||||
name = Column(String(32))
|
||||
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
|
||||
processes = relationship("Process", back_populates="equipment_roles", secondary=equipmentroles_processes)
|
||||
processes = relationship("Process", back_populates='equipment_roles', secondary=equipmentroles_processes)
|
||||
|
||||
equipmentrole_submissiontype_associations = relationship(
|
||||
"SubmissionTypeEquipmentRoleAssociation",
|
||||
@@ -1081,12 +990,24 @@ class EquipmentRole(BaseClass):
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EquipmentRole({self.name})>"
|
||||
|
||||
def to_dict(self):
|
||||
output = {}
|
||||
for key, value in self.__dict__.items():
|
||||
match key:
|
||||
case "processes":
|
||||
pass
|
||||
case _:
|
||||
value = value
|
||||
output[key] = value
|
||||
return output
|
||||
|
||||
def to_pydantic(self, submission_type:SubmissionType):
|
||||
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
|
||||
from backend.validators.pydant import PydEquipmentRole
|
||||
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
|
||||
pyd_dict = self.__dict__
|
||||
pyd_dict['processes'] = self.get_processes(submission_type=submission_type)
|
||||
equipment = [item.to_pydantic(submission_type=submission_type, extraction_kit=extraction_kit) for item in self.instances]
|
||||
# processes = [item.name for item in self.processes]
|
||||
pyd_dict = self.to_dict()
|
||||
pyd_dict['processes'] = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit)
|
||||
return PydEquipmentRole(equipment=equipment, **pyd_dict)
|
||||
|
||||
@classmethod
|
||||
@@ -1107,31 +1028,36 @@ class EquipmentRole(BaseClass):
|
||||
pass
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def get_processes(self, submission_type:str|SubmissionType|None) -> List[Process]:
|
||||
def get_processes(self, submission_type:str|SubmissionType|None, extraction_kit:str|KitType|None=None) -> List[Process]:
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
# assert all([isinstance(process, Process) for process in self.processes])
|
||||
# logger.debug(self.processes)
|
||||
if submission_type != None:
|
||||
output = [process.name for process in self.processes if submission_type in process.submission_types]
|
||||
# for process in self.processes:
|
||||
# logger.debug(f"Process: {type(process)}: {process}")
|
||||
processes = [process for process in self.processes if submission_type in process.submission_types]
|
||||
else:
|
||||
output = [process.name for process in self.processes]
|
||||
processes = self.processes
|
||||
match extraction_kit:
|
||||
case str():
|
||||
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
|
||||
case KitType():
|
||||
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
|
||||
case _:
|
||||
pass
|
||||
output = [item.name for item in processes]
|
||||
if len(output) == 0:
|
||||
return ['']
|
||||
else:
|
||||
return output
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
except:
|
||||
self.__database_session__.rollback()
|
||||
|
||||
class SubmissionEquipmentAssociation(BaseClass):
|
||||
|
||||
# Currently abstract until ready to implement
|
||||
# __abstract__ = True
|
||||
|
||||
__tablename__ = "_equipment_submissions"
|
||||
# __tablename__ = "_equipment_submissions"
|
||||
|
||||
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
|
||||
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
|
||||
@@ -1144,14 +1070,14 @@ class SubmissionEquipmentAssociation(BaseClass):
|
||||
|
||||
submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission
|
||||
|
||||
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated submission
|
||||
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
|
||||
|
||||
def __init__(self, submission, equipment):
|
||||
self.submission = submission
|
||||
self.equipment = equipment
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=self.process.name, role=self.role, nickname=self.equipment.nickname)
|
||||
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, processes=[self.process.name], role=self.role, nickname=self.equipment.nickname)
|
||||
return output
|
||||
|
||||
def save(self):
|
||||
@@ -1162,7 +1088,7 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
|
||||
|
||||
# __abstract__ = True
|
||||
|
||||
__tablename__ = "_submissiontype_equipmentrole"
|
||||
# __tablename__ = "_submissiontype_equipmentrole"
|
||||
|
||||
equipmentrole_id = Column(INTEGER, ForeignKey("_equipment_roles.id"), primary_key=True) #: id of associated equipment
|
||||
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
|
||||
@@ -1192,25 +1118,38 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
|
||||
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
||||
return value
|
||||
|
||||
def get_all_processes(self):
|
||||
def get_all_processes(self, extraction_kit:KitType|str|None=None):
|
||||
processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances]
|
||||
# flatten list
|
||||
processes = [item for items in processes for item in items if item != None ]
|
||||
match extraction_kit:
|
||||
case str():
|
||||
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
|
||||
case KitType():
|
||||
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
|
||||
case _:
|
||||
pass
|
||||
return processes
|
||||
|
||||
@check_authorization
|
||||
def save(self, ctx:Settings):
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
# self.__database_session__.add(self)
|
||||
# self.__database_session__.commit()
|
||||
super().save()
|
||||
|
||||
class Process(BaseClass):
|
||||
|
||||
__tablename__ = "_process"
|
||||
"""
|
||||
A Process is a method used by a piece of equipment.
|
||||
"""
|
||||
# __tablename__ = "_process"
|
||||
|
||||
id = Column(INTEGER, primary_key=True)
|
||||
name = Column(String(64))
|
||||
submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes)
|
||||
equipment = relationship("Equipment", back_populates='processes', secondary=equipment_processes)
|
||||
equipment_roles = relationship("EquipmentRole", back_populates='processes', secondary=equipmentroles_processes)
|
||||
submissions = relationship("SubmissionEquipmentAssociation", backref='process')
|
||||
kit_types = relationship("KitType", back_populates='processes', secondary=kittypes_processes)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Process({self.name})"
|
||||
@@ -1227,124 +1166,3 @@ class Process(BaseClass):
|
||||
pass
|
||||
return query_return(query=query, limit=limit)
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
except:
|
||||
self.__database_session__.rollback()
|
||||
|
||||
# class KitTypeReagentTypeAssociation(BaseClass):
|
||||
# """
|
||||
# table containing reagenttype/kittype associations
|
||||
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||
# """
|
||||
# __tablename__ = "_reagenttypes_kittypes"
|
||||
|
||||
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
|
||||
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
|
||||
# uses = Column(JSON) #: map to location on excel sheets of different submission types
|
||||
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
|
||||
# last_used = Column(String(32)) #: last used lot number of this type of reagent
|
||||
|
||||
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
|
||||
|
||||
# # reference to the "ReagentType" object
|
||||
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
|
||||
|
||||
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
|
||||
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
|
||||
# self.kit_type = kit_type
|
||||
# self.reagent_type = reagent_type
|
||||
# self.uses = uses
|
||||
# self.required = required
|
||||
|
||||
# def __repr__(self) -> str:
|
||||
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
|
||||
|
||||
# @validates('required')
|
||||
# def validate_age(self, key, value):
|
||||
# """
|
||||
# Ensures only 1 & 0 used in 'required'
|
||||
|
||||
# Args:
|
||||
# key (str): name of attribute
|
||||
# value (_type_): value of attribute
|
||||
|
||||
# Raises:
|
||||
# ValueError: Raised if bad value given
|
||||
|
||||
# Returns:
|
||||
# _type_: value
|
||||
# """
|
||||
# if not 0 <= value < 2:
|
||||
# raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
|
||||
# return value
|
||||
|
||||
# @validates('reagenttype')
|
||||
# def validate_reagenttype(self, key, value):
|
||||
# """
|
||||
# Ensures reagenttype is an actual ReagentType
|
||||
|
||||
# Args:
|
||||
# key (str)): name of attribute
|
||||
# value (_type_): value of attribute
|
||||
|
||||
# Raises:
|
||||
# ValueError: raised if reagenttype is not a ReagentType
|
||||
|
||||
# Returns:
|
||||
# _type_: ReagentType
|
||||
# """
|
||||
# if not isinstance(value, ReagentType):
|
||||
# raise ValueError(f'{value} is not a reagenttype')
|
||||
# return value
|
||||
|
||||
# @classmethod
|
||||
# @setup_lookup
|
||||
# def query(cls,
|
||||
# kit_type:KitType|str|None=None,
|
||||
# reagent_type:ReagentType|str|None=None,
|
||||
# limit:int=0
|
||||
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
|
||||
# """
|
||||
# Lookup junction of ReagentType and KitType
|
||||
|
||||
# Args:
|
||||
# kit_type (models.KitType | str | None): KitType of interest.
|
||||
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
|
||||
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
|
||||
|
||||
# Returns:
|
||||
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
|
||||
# """
|
||||
# query: Query = cls.__database_session__.query(cls)
|
||||
# match kit_type:
|
||||
# case KitType():
|
||||
# query = query.filter(cls.kit_type==kit_type)
|
||||
# case str():
|
||||
# query = query.join(KitType).filter(KitType.name==kit_type)
|
||||
# case _:
|
||||
# pass
|
||||
# match reagent_type:
|
||||
# case ReagentType():
|
||||
# query = query.filter(cls.reagent_type==reagent_type)
|
||||
# case str():
|
||||
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
|
||||
# case _:
|
||||
# pass
|
||||
# if kit_type != None and reagent_type != None:
|
||||
# limit = 1
|
||||
# return query_return(query=query, limit=limit)
|
||||
|
||||
# def save(self) -> Report:
|
||||
# """
|
||||
# Adds this instance to the database and commits.
|
||||
|
||||
# Returns:
|
||||
# Report: Result of save action
|
||||
# """
|
||||
# report = Report()
|
||||
# self.__database_session__.add(self)
|
||||
# self.__database_session__.commit()
|
||||
# return report
|
||||
|
||||
@@ -25,7 +25,7 @@ class Organization(BaseClass):
|
||||
"""
|
||||
Base of organization
|
||||
"""
|
||||
__tablename__ = "_organizations"
|
||||
# __tablename__ = "_organizations"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64)) #: organization name
|
||||
@@ -73,14 +73,13 @@ class Organization(BaseClass):
|
||||
Args:
|
||||
ctx (Settings): Settings object passed down from GUI. Necessary to check authorization
|
||||
"""
|
||||
ctx.database_session.add(self)
|
||||
ctx.database_session.commit()
|
||||
super().save()
|
||||
|
||||
class Contact(BaseClass):
|
||||
"""
|
||||
Base of Contact
|
||||
"""
|
||||
__tablename__ = "_contacts"
|
||||
# __tablename__ = "_contacts"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64)) #: contact name
|
||||
|
||||
@@ -13,23 +13,24 @@ from json.decoder import JSONDecodeError
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
import pandas as pd
|
||||
from openpyxl import Workbook
|
||||
from . import BaseClass
|
||||
from . import BaseClass, Equipment
|
||||
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
|
||||
from datetime import datetime, date, time
|
||||
from typing import List
|
||||
from typing import List, Any
|
||||
from dateutil.parser import parse
|
||||
from dateutil.parser._parser import ParserError
|
||||
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
|
||||
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class BasicSubmission(BaseClass):
|
||||
"""
|
||||
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
|
||||
"""
|
||||
__tablename__ = "_submissions"
|
||||
# __tablename__ = "_submissions"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
|
||||
@@ -96,7 +97,7 @@ class BasicSubmission(BaseClass):
|
||||
"""
|
||||
return f"{self.rsl_plate_num} - {self.submitter_plate_num}"
|
||||
|
||||
def to_dict(self, full_data:bool=False) -> dict:
|
||||
def to_dict(self, full_data:bool=False, backup:bool=False) -> dict:
|
||||
"""
|
||||
Constructs dictionary used in submissions summary
|
||||
|
||||
@@ -137,7 +138,7 @@ class BasicSubmission(BaseClass):
|
||||
logger.error(f"We got an error retrieving reagents: {e}")
|
||||
reagents = None
|
||||
# samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations]
|
||||
samples = [item.to_sub_dict() for item in self.submission_sample_associations]
|
||||
samples = self.adjust_to_dict_samples(backup=backup)
|
||||
try:
|
||||
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
|
||||
if len(equipment) == 0:
|
||||
@@ -255,18 +256,7 @@ class BasicSubmission(BaseClass):
|
||||
Returns:
|
||||
list: list of htipick dictionaries for each sample
|
||||
"""
|
||||
output_list = []
|
||||
for assoc in self.submission_sample_associations:
|
||||
samp = assoc.sample.to_hitpick(submission_rsl=self.rsl_plate_num)
|
||||
if samp != None:
|
||||
if plate_number != None:
|
||||
samp['plate_number'] = plate_number
|
||||
samp['row'] = assoc.row
|
||||
samp['column'] = assoc.column
|
||||
samp['plate_name'] = self.rsl_plate_num
|
||||
output_list.append(samp)
|
||||
else:
|
||||
continue
|
||||
output_list = [assoc.to_hitpick() for assoc in self.submission_sample_associations]
|
||||
return output_list
|
||||
|
||||
@classmethod
|
||||
@@ -548,7 +538,7 @@ class BasicSubmission(BaseClass):
|
||||
result = assoc.save()
|
||||
return result
|
||||
|
||||
def to_pydantic(self):
|
||||
def to_pydantic(self, backup:bool=False):
|
||||
"""
|
||||
Converts this instance into a PydSubmission
|
||||
|
||||
@@ -556,7 +546,7 @@ class BasicSubmission(BaseClass):
|
||||
PydSubmission: converted object.
|
||||
"""
|
||||
from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment
|
||||
dicto = self.to_dict(full_data=True)
|
||||
dicto = self.to_dict(full_data=True, backup=backup)
|
||||
logger.debug(f"Backup dictionary: {pformat(dicto)}")
|
||||
# dicto['filepath'] = Path(tempfile.TemporaryFile().name)
|
||||
new_dict = {}
|
||||
@@ -567,7 +557,11 @@ class BasicSubmission(BaseClass):
|
||||
case "samples":
|
||||
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
|
||||
case "equipment":
|
||||
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||
# logger.debug(f"\n\nEquipment: {dicto['equipment']}\n\n")
|
||||
try:
|
||||
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||
except TypeError as e:
|
||||
logger.error(f"Possible no equipment error: {e}")
|
||||
case "Plate Number":
|
||||
new_dict['rsl_plate_num'] = dict(value=value, missing=True)
|
||||
case "Submitter Plate Number":
|
||||
@@ -582,25 +576,6 @@ class BasicSubmission(BaseClass):
|
||||
# sys.exit()
|
||||
return PydSubmission(**new_dict)
|
||||
|
||||
def backup(self, fname:Path, full_backup:bool=True):
|
||||
"""
|
||||
Exports xlsx and yml info files for this instance.
|
||||
|
||||
Args:
|
||||
fname (Path): Filename of xlsx file.
|
||||
"""
|
||||
if full_backup:
|
||||
backup = self.to_dict(full_data=True)
|
||||
try:
|
||||
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
|
||||
yaml.dump(backup, f)
|
||||
except KeyError as e:
|
||||
logger.error(f"Problem saving yml backup file: {e}")
|
||||
pyd = self.to_pydantic()
|
||||
wb = pyd.autofill_excel()
|
||||
wb = pyd.autofill_samples(wb)
|
||||
wb.save(filename=fname.with_suffix(".xlsx"))
|
||||
|
||||
def save(self, original:bool=True):
|
||||
"""
|
||||
Adds this instance to database and commits.
|
||||
@@ -610,24 +585,7 @@ class BasicSubmission(BaseClass):
|
||||
"""
|
||||
if original:
|
||||
self.uploaded_by = getuser()
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Performs backup and deletes this instance from database.
|
||||
|
||||
Raises:
|
||||
e: Raised in something goes wrong.
|
||||
"""
|
||||
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
|
||||
self.backup(fname=fname)
|
||||
self.__database_session__.delete(self)
|
||||
try:
|
||||
self.__database_session__.commit()
|
||||
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
|
||||
self.__database_session__.rollback()
|
||||
raise e
|
||||
super().save()
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
@@ -782,15 +740,121 @@ class BasicSubmission(BaseClass):
|
||||
def get_used_equipment(self) -> List[str]:
|
||||
return [item.role for item in self.submission_equipment_associations]
|
||||
|
||||
@classmethod
|
||||
def adjust_autofill_samples(cls, samples:List[Any]) -> List[Any]:
|
||||
logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler")
|
||||
return samples
|
||||
|
||||
def adjust_to_dict_samples(self, backup:bool=False):
|
||||
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
|
||||
return [item.to_sub_dict() for item in self.submission_sample_associations]
|
||||
|
||||
# Custom context events for the ui
|
||||
|
||||
def custom_context_events(self):
|
||||
names = ["Delete", "Details", "Add Comment", "Add Equipment", "Export"]
|
||||
funcs = [self.delete, self.show_details, self.add_comment, self.add_equipment, self.backup]
|
||||
dicto = {item[0]:item[1] for item in zip(names, funcs)}
|
||||
return dicto
|
||||
|
||||
def delete(self, obj=None):
|
||||
"""
|
||||
Performs backup and deletes this instance from database.
|
||||
|
||||
Raises:
|
||||
e: Raised in something goes wrong.
|
||||
"""
|
||||
logger.debug("Hello from delete")
|
||||
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
|
||||
self.backup(fname=fname, full_backup=True)
|
||||
self.__database_session__.delete(self)
|
||||
try:
|
||||
self.__database_session__.commit()
|
||||
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
|
||||
self.__database_session__.rollback()
|
||||
raise e
|
||||
|
||||
def show_details(self, obj):
|
||||
logger.debug("Hello from details")
|
||||
from frontend.widgets.submission_details import SubmissionDetails
|
||||
dlg = SubmissionDetails(parent=obj, sub=self)
|
||||
if dlg.exec():
|
||||
pass
|
||||
|
||||
def add_comment(self, obj):
|
||||
from frontend.widgets.submission_details import SubmissionComment
|
||||
dlg = SubmissionComment(parent=obj, submission=self)
|
||||
if dlg.exec():
|
||||
comment = dlg.parse_form()
|
||||
try:
|
||||
# For some reason .append results in new comment being ignored, so have to concatenate lists.
|
||||
self.comment = self.comment + comment
|
||||
except (AttributeError, TypeError) as e:
|
||||
logger.error(f"Hit error ({e}) creating comment")
|
||||
self.comment = comment
|
||||
logger.debug(self.comment)
|
||||
self.save(original=False)
|
||||
# logger.debug(f"Save result: {result}")
|
||||
|
||||
def add_equipment(self, obj):
|
||||
# submission_type = submission.submission_type_name
|
||||
from frontend.widgets.equipment_usage import EquipmentUsage
|
||||
dlg = EquipmentUsage(parent=obj, submission_type=self.submission_type_name, submission=self)
|
||||
if dlg.exec():
|
||||
equipment = dlg.parse_form()
|
||||
logger.debug(f"We've got equipment: {equipment}")
|
||||
for equip in equipment:
|
||||
# e = Equipment.query(name=equip.name)
|
||||
# assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
||||
# process = Process.query(name=equip.processes)
|
||||
# assoc.process = process
|
||||
# assoc.role = equip.role
|
||||
_, assoc = equip.toSQL(submission=self)
|
||||
# submission.submission_equipment_associations.append(assoc)
|
||||
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
||||
# submission.save()
|
||||
assoc.save()
|
||||
else:
|
||||
pass
|
||||
|
||||
def backup(self, obj=None, fname:Path|None=None, full_backup:bool=False):
|
||||
"""
|
||||
Exports xlsx and yml info files for this instance.
|
||||
|
||||
Args:
|
||||
fname (Path): Filename of xlsx file.
|
||||
"""
|
||||
logger.debug("Hello from backup.")
|
||||
if fname == None:
|
||||
from frontend.widgets.functions import select_save_file
|
||||
from backend.validators import RSLNamer
|
||||
abbreviation = self.get_abbreviation()
|
||||
file_data = dict(rsl_plate_num=self.rsl_plate_num, submission_type=self.submission_type_name, submitted_date=self.submitted_date, abbreviation=abbreviation)
|
||||
fname = select_save_file(default_name=RSLNamer.construct_new_plate_name(data=file_data), extension="xlsx", obj=obj)
|
||||
if full_backup:
|
||||
backup = self.to_dict(full_data=True)
|
||||
try:
|
||||
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
|
||||
yaml.dump(backup, f)
|
||||
except KeyError as e:
|
||||
logger.error(f"Problem saving yml backup file: {e}")
|
||||
pyd = self.to_pydantic(backup=True)
|
||||
wb = pyd.autofill_excel()
|
||||
wb = pyd.autofill_samples(wb)
|
||||
wb = pyd.autofill_equipment(wb)
|
||||
wb.save(filename=fname.with_suffix(".xlsx"))
|
||||
|
||||
# Below are the custom submission types
|
||||
|
||||
class BacterialCulture(BasicSubmission):
|
||||
"""
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
|
||||
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
|
||||
controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission
|
||||
__mapper_args__ = {"polymorphic_identity": "Bacterial Culture", "polymorphic_load": "inline"}
|
||||
__mapper_args__ = dict(polymorphic_identity="Bacterial Culture",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSubmission.id))
|
||||
|
||||
def to_dict(self, full_data:bool=False) -> dict:
|
||||
"""
|
||||
@@ -804,6 +868,10 @@ class BacterialCulture(BasicSubmission):
|
||||
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_abbreviation(cls):
|
||||
return "BC"
|
||||
|
||||
@classmethod
|
||||
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
@@ -853,7 +921,7 @@ class BacterialCulture(BasicSubmission):
|
||||
Extends parent
|
||||
"""
|
||||
from backend.validators import RSLNamer
|
||||
data['abbreviation'] = "BC"
|
||||
data['abbreviation'] = cls.get_abbreviation()
|
||||
outstr = super().enforce_name(instr=instr, data=data)
|
||||
# def construct(data:dict|None=None) -> str:
|
||||
# """
|
||||
@@ -932,10 +1000,12 @@ class Wastewater(BasicSubmission):
|
||||
"""
|
||||
derivative submission type from BasicSubmission
|
||||
"""
|
||||
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
|
||||
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
|
||||
ext_technician = Column(String(64))
|
||||
pcr_technician = Column(String(64))
|
||||
__mapper_args__ = {"polymorphic_identity": "Wastewater", "polymorphic_load": "inline"}
|
||||
__mapper_args__ = __mapper_args__ = dict(polymorphic_identity="Wastewater",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSubmission.id))
|
||||
|
||||
def to_dict(self, full_data:bool=False) -> dict:
|
||||
"""
|
||||
@@ -952,6 +1022,10 @@ class Wastewater(BasicSubmission):
|
||||
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def get_abbreviation(cls):
|
||||
return "WW"
|
||||
|
||||
@classmethod
|
||||
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||
"""
|
||||
@@ -1012,7 +1086,7 @@ class Wastewater(BasicSubmission):
|
||||
Extends parent
|
||||
"""
|
||||
from backend.validators import RSLNamer
|
||||
data['abbreviation'] = "WW"
|
||||
data['abbreviation'] = cls.get_abbreviation()
|
||||
outstr = super().enforce_name(instr=instr, data=data)
|
||||
# def construct(data:dict|None=None):
|
||||
# if "submitted_date" in data.keys():
|
||||
@@ -1066,13 +1140,21 @@ class Wastewater(BasicSubmission):
|
||||
# return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)"
|
||||
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
|
||||
|
||||
@classmethod
|
||||
def adjust_autofill_samples(cls, samples: List[Any]) -> List[Any]:
|
||||
samples = super().adjust_autofill_samples(samples)
|
||||
return [item for item in samples if not item.submitter_id.startswith("EN")]
|
||||
|
||||
class WastewaterArtic(BasicSubmission):
|
||||
"""
|
||||
derivative submission type for artic wastewater
|
||||
"""
|
||||
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
|
||||
__mapper_args__ = {"polymorphic_identity": "Wastewater Artic", "polymorphic_load": "inline"}
|
||||
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
|
||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSubmission.id))
|
||||
artic_technician = Column(String(64))
|
||||
dna_core_submission_number = Column(String(64))
|
||||
|
||||
def calculate_base_cost(self):
|
||||
"""
|
||||
@@ -1093,6 +1175,10 @@ class WastewaterArtic(BasicSubmission):
|
||||
except Exception as e:
|
||||
logger.error(f"Calculation error: {e}")
|
||||
|
||||
@classmethod
|
||||
def get_abbreviation(cls):
|
||||
return "AR"
|
||||
|
||||
@classmethod
|
||||
def parse_samples(cls, input_dict: dict) -> dict:
|
||||
"""
|
||||
@@ -1109,15 +1195,45 @@ class WastewaterArtic(BasicSubmission):
|
||||
# Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
|
||||
# at the end, this has to be done here. No moving to sqlalchemy object :(
|
||||
input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip()
|
||||
if "ENC" in input_dict['submitter_id']:
|
||||
input_dict['submitter_id'] = cls.en_adapter(input_str=input_dict['submitter_id'])
|
||||
return input_dict
|
||||
|
||||
@classmethod
|
||||
def en_adapter(cls, input_str) -> str:
|
||||
processed = re.sub(r"[A-Z]", "", input_str)
|
||||
try:
|
||||
en_num = re.search(r"\-\d{1}$", processed).group()
|
||||
processed = processed.replace(en_num, "", -1)
|
||||
except AttributeError:
|
||||
en_num = "1"
|
||||
en_num = en_num.strip("-")
|
||||
logger.debug(f"Processed after en-num: {processed}")
|
||||
try:
|
||||
plate_num = re.search(r"\-\d{1}$", processed).group()
|
||||
processed = processed.replace(plate_num, "", -1)
|
||||
except AttributeError:
|
||||
plate_num = "1"
|
||||
plate_num = plate_num.strip("-")
|
||||
logger.debug(f"Processed after plate-num: {processed}")
|
||||
day = re.search(r"\d{2}$", processed).group()
|
||||
processed = processed.replace(day, "", -1)
|
||||
logger.debug(f"Processed after day: {processed}")
|
||||
month = re.search(r"\d{2}$", processed).group()
|
||||
processed = processed.replace(month, "", -1)
|
||||
processed = processed.replace("--", "")
|
||||
logger.debug(f"Processed after month: {processed}")
|
||||
year = re.search(r'^(?:\d{2})?\d{2}', processed).group()
|
||||
year = f"20{year}"
|
||||
return f"EN{year}{month}{day}-{en_num}"
|
||||
|
||||
@classmethod
|
||||
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
|
||||
"""
|
||||
Extends parent
|
||||
"""
|
||||
from backend.validators import RSLNamer
|
||||
data['abbreviation'] = "AR"
|
||||
data['abbreviation'] = cls.get_abbreviation()
|
||||
outstr = super().enforce_name(instr=instr, data=data)
|
||||
# def construct(data:dict|None=None):
|
||||
# today = datetime.now()
|
||||
@@ -1240,6 +1356,36 @@ class WastewaterArtic(BasicSubmission):
|
||||
worksheet.cell(row=iii, column=jjj, value=value)
|
||||
return input_excel
|
||||
|
||||
def adjust_to_dict_samples(self, backup:bool=False):
|
||||
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
|
||||
if backup:
|
||||
output = []
|
||||
for assoc in self.submission_sample_associations:
|
||||
dicto = assoc.to_sub_dict()
|
||||
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
|
||||
try:
|
||||
dicto['plate_name'] = old_sub.rsl_plate_num
|
||||
except AttributeError:
|
||||
dicto['plate_name'] = ""
|
||||
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
|
||||
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
|
||||
output.append(dicto)
|
||||
else:
|
||||
output = super().adjust_to_dict_samples(backup=False)
|
||||
return output
|
||||
|
||||
def custom_context_events(self):
|
||||
events = super().custom_context_events()
|
||||
events['Gel Box'] = self.gel_box
|
||||
return events
|
||||
|
||||
def gel_box(self, obj):
|
||||
from frontend.widgets.gel_checker import GelBox
|
||||
dlg = GelBox(parent=obj)
|
||||
if dlg.exec():
|
||||
output = dlg.parse_form()
|
||||
print(output)
|
||||
|
||||
# Sample Classes
|
||||
|
||||
class BasicSample(BaseClass):
|
||||
@@ -1247,7 +1393,7 @@ class BasicSample(BaseClass):
|
||||
Base of basic sample which polymorphs into BCSample and WWSample
|
||||
"""
|
||||
|
||||
__tablename__ = "_samples"
|
||||
# __tablename__ = "_samples"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
|
||||
@@ -1295,6 +1441,18 @@ class BasicSample(BaseClass):
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.sample_type.replace('_', ' ').title().replace(' ', '')}({self.submitter_id})>"
|
||||
|
||||
def to_sub_dict(self, submission_rsl:str) -> dict:
|
||||
"""
|
||||
gui friendly dictionary, extends parent method.
|
||||
|
||||
Returns:
|
||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||
"""
|
||||
sample = {}
|
||||
sample['submitter_id'] = self.submitter_id
|
||||
sample['sample_type'] = self.sample_type
|
||||
return sample
|
||||
|
||||
def set_attribute(self, name:str, value):
|
||||
"""
|
||||
Custom attribute setter
|
||||
@@ -1307,59 +1465,6 @@ class BasicSample(BaseClass):
|
||||
setattr(self, name, value)
|
||||
except AttributeError:
|
||||
logger.error(f"Attribute {name} not found")
|
||||
|
||||
def to_sub_dict(self, submission_rsl:str|BasicSubmission) -> dict:
|
||||
"""
|
||||
Returns a dictionary of locations.
|
||||
|
||||
Args:
|
||||
submission_rsl (str): Submission RSL number.
|
||||
|
||||
Returns:
|
||||
dict: 'well' and sample submitter_id as 'name'
|
||||
"""
|
||||
match submission_rsl:
|
||||
case BasicSubmission():
|
||||
assoc = [item for item in self.sample_submission_associations if item.submission==submission_rsl][0]
|
||||
case str():
|
||||
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||
sample = {}
|
||||
try:
|
||||
sample['well'] = f"{row_map[assoc.row]}{assoc.column}"
|
||||
except KeyError as e:
|
||||
logger.error(f"Unable to find row {assoc.row} in row_map.")
|
||||
sample['well'] = None
|
||||
sample['name'] = self.submitter_id
|
||||
sample['submitter_id'] = self.submitter_id
|
||||
sample['sample_type'] = self.sample_type
|
||||
if isinstance(assoc.row, list):
|
||||
sample['row'] = assoc.row[0]
|
||||
else:
|
||||
sample['row'] = assoc.row
|
||||
if isinstance(assoc.column, list):
|
||||
sample['column'] = assoc.column[0]
|
||||
else:
|
||||
sample['column'] = assoc.column
|
||||
return sample
|
||||
|
||||
def to_hitpick(self, submission_rsl:str|None=None) -> dict|None:
|
||||
"""
|
||||
Outputs a dictionary usable for html plate maps.
|
||||
|
||||
Returns:
|
||||
dict: dictionary of sample id, row and column in elution plate
|
||||
"""
|
||||
# Since there is no PCR, negliable result is necessary.
|
||||
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||
fields = self.to_sub_dict(submission_rsl=submission_rsl)
|
||||
env = jinja_template_loading()
|
||||
template = env.get_template("tooltip.html")
|
||||
tooltip_text = template.render(fields=fields)
|
||||
# tooltip_text = f"""
|
||||
# Sample name: {self.submitter_id}<br>
|
||||
# Well: {row_map[assoc.row]}{assoc.column}
|
||||
# """
|
||||
return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
|
||||
|
||||
@classmethod
|
||||
def find_subclasses(cls, attrs:dict|None=None, sample_type:str|None=None) -> BasicSample:
|
||||
@@ -1507,11 +1612,6 @@ class BasicSample(BaseClass):
|
||||
logger.debug(f"Creating instance: {instance}")
|
||||
return instance
|
||||
|
||||
def save(self):
|
||||
# raise AttributeError(f"Save not implemented for {self.__class__}")
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
|
||||
def delete(self):
|
||||
raise AttributeError(f"Delete not implemented for {self.__class__}")
|
||||
|
||||
@@ -1521,7 +1621,7 @@ class WastewaterSample(BasicSample):
|
||||
"""
|
||||
Derivative wastewater sample
|
||||
"""
|
||||
# id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True)
|
||||
id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
|
||||
ww_processing_num = Column(String(64)) #: wastewater processing number
|
||||
ww_full_sample_id = Column(String(64)) #: full id given by entrics
|
||||
rsl_number = Column(String(64)) #: rsl plate identification number
|
||||
@@ -1529,46 +1629,21 @@ class WastewaterSample(BasicSample):
|
||||
received_date = Column(TIMESTAMP) #: Date sample received
|
||||
notes = Column(String(2000)) #: notes from submission form
|
||||
sample_location = Column(String(8)) #: location on 24 well plate
|
||||
__mapper_args__ = {"polymorphic_identity": "Wastewater Sample", "polymorphic_load": "inline"}
|
||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Sample",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSample.id))
|
||||
|
||||
def to_hitpick(self, submission_rsl:str) -> dict|None:
|
||||
def to_sub_dict(self, submission_rsl:str) -> dict:
|
||||
"""
|
||||
Outputs a dictionary usable for html plate maps. Extends parent method.
|
||||
|
||||
Args:
|
||||
submission_rsl (str): rsl_plate_num of the submission
|
||||
gui friendly dictionary, extends parent method.
|
||||
|
||||
Returns:
|
||||
dict|None: dict: dictionary of sample id, row and column in elution plate
|
||||
"""
|
||||
sample = super().to_hitpick(submission_rsl=submission_rsl)
|
||||
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||
# if either n1 or n2 is positive, include this sample
|
||||
try:
|
||||
sample['positive'] = any(["positive" in item for item in [assoc.n1_status, assoc.n2_status]])
|
||||
except (TypeError, AttributeError) as e:
|
||||
logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
|
||||
try:
|
||||
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(assoc.ct_n1)} ({assoc.n1_status})<br>- ct N2: {'{:.2f}'.format(assoc.ct_n2)} ({assoc.n2_status})"
|
||||
except (TypeError, AttributeError) as e:
|
||||
logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
|
||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||
"""
|
||||
sample = super().to_sub_dict(submission_rsl=submission_rsl)
|
||||
sample['ww_processing_num'] = self.ww_processing_num
|
||||
return sample
|
||||
|
||||
def get_recent_ww_submission(self) -> Wastewater:
|
||||
"""
|
||||
Gets most recent associated wastewater submission
|
||||
|
||||
Returns:
|
||||
Wastewater: Most recent wastewater submission
|
||||
"""
|
||||
results = [sub for sub in self.submissions if isinstance(sub, Wastewater)]
|
||||
if len(results) > 1:
|
||||
results = results.sort(key=lambda sub: sub.submitted_date)
|
||||
try:
|
||||
return results[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def parse_sample(cls, input_dict: dict) -> dict:
|
||||
output_dict = super().parse_sample(input_dict)
|
||||
@@ -1591,35 +1666,28 @@ class WastewaterSample(BasicSample):
|
||||
case _:
|
||||
del output_dict['collection_date']
|
||||
return output_dict
|
||||
|
||||
def to_sub_dict(self, submission_rsl: str | BasicSubmission) -> dict:
|
||||
sample = super().to_sub_dict(submission_rsl)
|
||||
if self.ww_processing_num != None:
|
||||
sample['ww_processing_num'] = self.ww_processing_num
|
||||
else:
|
||||
sample['ww_processing_num'] = self.submitter_id
|
||||
|
||||
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
|
||||
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
|
||||
subs = self.submissions[:self.submissions.index(current_artic_submission)]
|
||||
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
|
||||
logger.debug(f"Submissions up to current artic submission: {subs}")
|
||||
try:
|
||||
assoc = [item for item in self.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
||||
except:
|
||||
assoc = None
|
||||
if assoc != None:
|
||||
try:
|
||||
sample['ct'] = f"{assoc.ct_n1:.2f}, {assoc.ct_n2:.2f}"
|
||||
except TypeError:
|
||||
sample['ct'] = "None, None"
|
||||
sample['source_plate'] = assoc.submission.rsl_plate_num
|
||||
sample['source_well'] = f"{row_map[assoc.row]}{assoc.column}"
|
||||
return sample
|
||||
return subs[-1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
class BacterialCultureSample(BasicSample):
|
||||
"""
|
||||
base of bacterial culture sample
|
||||
"""
|
||||
# id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True)
|
||||
id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
|
||||
organism = Column(String(64)) #: bacterial specimen
|
||||
concentration = Column(String(16)) #: sample concentration
|
||||
control = relationship("Control", back_populates="sample", uselist=False)
|
||||
__mapper_args__ = {"polymorphic_identity": "Bacterial Culture Sample", "polymorphic_load": "inline"}
|
||||
__mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(id == BasicSample.id))
|
||||
|
||||
def to_sub_dict(self, submission_rsl:str) -> dict:
|
||||
"""
|
||||
@@ -1632,15 +1700,18 @@ class BacterialCultureSample(BasicSample):
|
||||
sample['name'] = self.submitter_id
|
||||
sample['organism'] = self.organism
|
||||
sample['concentration'] = self.concentration
|
||||
return sample
|
||||
|
||||
def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
|
||||
sample = super().to_hitpick(submission_rsl)
|
||||
if self.control != None:
|
||||
sample['colour'] = [0,128,0]
|
||||
sample['tooltip'] += f"<br>- Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
||||
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
||||
return sample
|
||||
|
||||
# def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
|
||||
# sample = super().to_hitpick(submission_rsl)
|
||||
# if self.control != None:
|
||||
# sample['colour'] = [0,128,0]
|
||||
# sample['tooltip'] += f"<br>- Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
||||
# return sample
|
||||
|
||||
# Submission to Sample Associations
|
||||
|
||||
class SubmissionSampleAssociation(BaseClass):
|
||||
@@ -1649,7 +1720,7 @@ class SubmissionSampleAssociation(BaseClass):
|
||||
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||
"""
|
||||
|
||||
__tablename__ = "_submission_sample"
|
||||
# __tablename__ = "_submission_sample"
|
||||
|
||||
sample_id = Column(INTEGER, ForeignKey("_samples.id"), nullable=False) #: id of associated sample
|
||||
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
|
||||
@@ -1688,7 +1759,12 @@ class SubmissionSampleAssociation(BaseClass):
|
||||
Returns:
|
||||
dict: Updated dictionary with row, column and well updated
|
||||
"""
|
||||
# Get sample info
|
||||
sample = self.sample.to_sub_dict(submission_rsl=self.submission)
|
||||
# sample = {}
|
||||
sample['name'] = self.sample.submitter_id
|
||||
# sample['submitter_id'] = self.sample.submitter_id
|
||||
# sample['sample_type'] = self.sample.sample_type
|
||||
sample['row'] = self.row
|
||||
sample['column'] = self.column
|
||||
try:
|
||||
@@ -1696,6 +1772,33 @@ class SubmissionSampleAssociation(BaseClass):
|
||||
except KeyError as e:
|
||||
logger.error(f"Unable to find row {self.row} in row_map.")
|
||||
sample['well'] = None
|
||||
sample['plate_name'] = self.submission.rsl_plate_num
|
||||
sample['positive'] = False
|
||||
|
||||
return sample
|
||||
|
||||
def to_hitpick(self) -> dict|None:
|
||||
"""
|
||||
Outputs a dictionary usable for html plate maps.
|
||||
|
||||
Returns:
|
||||
dict: dictionary of sample id, row and column in elution plate
|
||||
"""
|
||||
# Since there is no PCR, negliable result is necessary.
|
||||
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
|
||||
sample = self.to_sub_dict()
|
||||
env = jinja_template_loading()
|
||||
template = env.get_template("tooltip.html")
|
||||
tooltip_text = template.render(fields=sample)
|
||||
try:
|
||||
tooltip_text += sample['tooltip']
|
||||
except KeyError:
|
||||
pass
|
||||
# tooltip_text = f"""
|
||||
# Sample name: {self.submitter_id}<br>
|
||||
# Well: {row_map[fields['row']]}{fields['column']}
|
||||
# """
|
||||
sample.update(dict(name=self.sample.submitter_id[:10], tooltip=tooltip_text))
|
||||
return sample
|
||||
|
||||
@classmethod
|
||||
@@ -1831,14 +1934,6 @@ class SubmissionSampleAssociation(BaseClass):
|
||||
instance = used_cls(submission=submission, sample=sample, **kwargs)
|
||||
return instance
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Adds this instance to the database and commits.
|
||||
"""
|
||||
self.__database_session__.add(self)
|
||||
self.__database_session__.commit()
|
||||
return None
|
||||
|
||||
def delete(self):
|
||||
raise AttributeError(f"Delete not implemented for {self.__class__}")
|
||||
|
||||
@@ -1846,10 +1941,32 @@ class WastewaterAssociation(SubmissionSampleAssociation):
|
||||
"""
|
||||
Derivative custom Wastewater/Submission Association... fancy.
|
||||
"""
|
||||
sample_id = Column(INTEGER, ForeignKey('_submissionsampleassociation.sample_id'), primary_key=True)
|
||||
submission_id = Column(INTEGER, ForeignKey('_submissionsampleassociation.submission_id'), primary_key=True)
|
||||
ct_n1 = Column(FLOAT(2)) #: AKA ct for N1
|
||||
ct_n2 = Column(FLOAT(2)) #: AKA ct for N2
|
||||
n1_status = Column(String(32)) #: positive or negative for N1
|
||||
n2_status = Column(String(32)) #: positive or negative for N2
|
||||
pcr_results = Column(JSON) #: imported PCR status from QuantStudio
|
||||
|
||||
__mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
|
||||
# __mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
|
||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Association",
|
||||
polymorphic_load="inline",
|
||||
inherit_condition=(sample_id == SubmissionSampleAssociation.sample_id))
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
sample = super().to_sub_dict()
|
||||
sample['ct'] = f"({self.ct_n1}, {self.ct_n2})"
|
||||
try:
|
||||
sample['positive'] = any(["positive" in item for item in [self.n1_status, self.n2_status]])
|
||||
except (TypeError, AttributeError) as e:
|
||||
logger.error(f"Couldn't check positives for {self.sample.rsl_number}. Looks like there isn't PCR data.")
|
||||
return sample
|
||||
|
||||
def to_hitpick(self) -> dict | None:
|
||||
sample = super().to_hitpick()
|
||||
try:
|
||||
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})<br>- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})"
|
||||
except (TypeError, AttributeError) as e:
|
||||
logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.")
|
||||
return sample
|
||||
|
||||
Reference in New Issue
Block a user