First working example.

This commit is contained in:
lwark
2025-05-05 14:47:50 -05:00
parent ea10798686
commit 5508f68bc8
7 changed files with 338 additions and 310 deletions

View File

@@ -24,28 +24,6 @@ Base: DeclarativeMeta = declarative_base()
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'submission_sample_associations',
'submission_reagent_associations', 'submission_equipment_associations',
'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls',
'source_plates']
__abstract__ = True
@property
def truncated_name(self):
name = str(self)
if len(name) > 64:
name = name.replace("<", "").replace(">", "")
if len(name) > 64:
# NOTE: As if re'agent'
name = name.replace("agent", "")
if len(name) > 64:
name = f"...{name[-61:]}"
return name
class BaseClass(Base): class BaseClass(Base):
""" """
Abstract class to pass ctx values to all SQLAlchemy objects. Abstract class to pass ctx values to all SQLAlchemy objects.
@@ -243,8 +221,10 @@ class BaseClass(Base):
Returns: Returns:
Any | List[Any]: Single result if limit = 1 or List if other. Any | List[Any]: Single result if limit = 1 or List if other.
""" """
logger.debug(f"Kwargs: {kwargs}")
if model is None: if model is None:
model = cls model = cls
logger.debug(f"Model: {model}")
if query is None: if query is None:
query: Query = cls.__database_session__.query(model) query: Query = cls.__database_session__.query(model)
singles = model.get_default_info('singles') singles = model.get_default_info('singles')
@@ -477,6 +457,28 @@ class BaseClass(Base):
return output_date return output_date
class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'submission_sample_associations',
'submission_reagent_associations', 'submission_equipment_associations',
'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls',
'source_plates']
__abstract__ = True
@property
def truncated_name(self):
name = str(self)
if len(name) > 64:
name = name.replace("<", "").replace(">", "")
if len(name) > 64:
# NOTE: As if re'agent'
name = name.replace("agent", "")
if len(name) > 64:
name = f"...{name[-61:]}"
return name
class ConfigItem(BaseClass): class ConfigItem(BaseClass):
""" """
Key:JSON objects to store config settings in database. Key:JSON objects to store config settings in database.

View File

@@ -123,6 +123,9 @@ class Control(BaseClass):
controltype = relationship("ControlType", back_populates="instances", controltype = relationship("ControlType", back_populates="instances",
foreign_keys=[controltype_name]) #: reference to parent control type foreign_keys=[controltype_name]) #: reference to parent control type
name = Column(String(255), unique=True) #: Sample ID name = Column(String(255), unique=True) #: Sample ID
sample_id = Column(String, ForeignKey("_basicsample.id", ondelete="SET NULL",
name="fk_Cont_sample_id")) #: name of joined submission type
sample = relationship("BasicSample", back_populates="control") #: This control's submission sample
submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id")) #: parent submission id submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id")) #: parent submission id
submission = relationship("BasicSubmission", back_populates="controls", submission = relationship("BasicSubmission", back_populates="controls",
@@ -362,7 +365,6 @@ class IridaControl(Control):
refseq_version = Column(String(16)) #: version of refseq used in fastq parsing refseq_version = Column(String(16)) #: version of refseq used in fastq parsing
kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing
kraken2_db_version = Column(String(32)) #: folder name of kraken2 db kraken2_db_version = Column(String(32)) #: folder name of kraken2 db
sample = relationship("BacterialCultureSample", back_populates="control") #: This control's submission sample
sample_id = Column(INTEGER, sample_id = Column(INTEGER,
ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key

View File

@@ -848,7 +848,7 @@ class SubmissionType(BaseClass):
name = Column(String(128), unique=True) #: name of submission type name = Column(String(128), unique=True) #: name of submission type
info_map = Column(JSON) #: Where parsable information is found in the excel workbook corresponding to this type. info_map = Column(JSON) #: Where parsable information is found in the excel workbook corresponding to this type.
defaults = Column(JSON) #: Basic information about this submission type defaults = Column(JSON) #: Basic information about this submission type
instances = relationship("BasicSubmission") #: Concrete instances of this type. instances = relationship("ClientSubmission", back_populates="submission_type") #: Concrete instances of this type.
template_file = Column(BLOB) #: Blank form for this type stored as binary. template_file = Column(BLOB) #: Blank form for this type stored as binary.
processes = relationship("Process", back_populates="submission_types", processes = relationship("Process", back_populates="submission_types",
secondary=submissiontypes_processes) #: Relation to equipment processes used for this type. secondary=submissiontypes_processes) #: Relation to equipment processes used for this type.
@@ -1048,9 +1048,9 @@ class SubmissionType(BaseClass):
} }
""" """
runtype_kit_associations = relationship( submissiontype_kit_associations = relationship(
"RunTypeKitTypeAssociation", "SubmissionTypeKitTypeAssociation",
back_populates="runtype", back_populates="submission_type",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) #: Association of kittypes ) #: Association of kittypes

View File

@@ -15,8 +15,8 @@ from operator import itemgetter
from pprint import pformat from pprint import pformat
from pandas import DataFrame from pandas import DataFrame
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from . import Base, BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin, \ from . import Base, BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, \
SubmissionReagentAssociation SubmissionReagentAssociation, LogMixin
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
@@ -55,14 +55,15 @@ class ClientSubmission(BaseClass, LogMixin):
String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name
sample_count = Column(INTEGER) #: Number of samples in the submission sample_count = Column(INTEGER) #: Number of samples in the submission
runs = relationship("BasicRun", back_populates="client_submission") #: many-to-one relationship runs = relationship("BasicSubmission", back_populates="client_submission") #: many-to-one relationship
contact = relationship("Contact", back_populates="submissions") #: client org contact = relationship("Contact", back_populates="submissions") #: client org
contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL", contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL",
name="fk_BS_contact_id")) #: client lab id from _organizations name="fk_BS_contact_id")) #: client lab id from _organizations
submission_type = relationship("SubmissionType", back_populates="instances") #: archetype of this submission
submission_type_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL", submission_type_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL",
name="fk_BS_subtype_name")) #: name of joined submission type name="fk_BS_subtype_name")) #: name of joined submission type
submission_type = relationship("SubmissionType", back_populates="instances") #: archetype of this submission
cost_centre = Column( cost_centre = Column(
String(64)) #: Permanent storage of used cost centre in case organization field changed in the future. String(64)) #: Permanent storage of used cost centre in case organization field changed in the future.
@@ -98,7 +99,9 @@ class BasicSubmission(BaseClass, LogMixin):
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_number = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012) rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
client_submission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL",
name="fk_BS_clientsub_id")) #: client lab id from _organizations)
client_submission = relationship("ClientSubmission", back_populates="runs") client_submission = relationship("ClientSubmission", back_populates="runs")
# submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab # submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab
started_date = Column(TIMESTAMP) #: Date this run was started. started_date = Column(TIMESTAMP) #: Date this run was started.
@@ -304,7 +307,8 @@ class BasicSubmission(BaseClass, LogMixin):
case SubmissionType(): case SubmissionType():
return sub_type return sub_type
case _: case _:
return SubmissionType.query(cls.__mapper_args__['polymorphic_identity']) # return SubmissionType.query(cls.__mapper_args__['polymorphic_identity'])
return None
@classmethod @classmethod
def construct_info_map(cls, submission_type: SubmissionType | None = None, def construct_info_map(cls, submission_type: SubmissionType | None = None,
@@ -356,7 +360,7 @@ class BasicSubmission(BaseClass, LogMixin):
""" """
# NOTE: get lab from nested organization object # NOTE: get lab from nested organization object
try: try:
sub_lab = self.submitting_lab.name sub_lab = self.client_submission.submitting_lab.name
except AttributeError: except AttributeError:
sub_lab = None sub_lab = None
try: try:
@@ -364,24 +368,24 @@ class BasicSubmission(BaseClass, LogMixin):
except AttributeError: except AttributeError:
pass pass
# NOTE: get extraction kit name from nested kit object # NOTE: get extraction kit name from nested kit object
try: # try:
ext_kit = self.extraction_kit.name # ext_kit = self.extraction_kit.name
except AttributeError: # except AttributeError:
ext_kit = None # ext_kit = None
# NOTE: load scraped extraction info # NOTE: load scraped extraction info
try: # try:
ext_info = self.extraction_info # ext_info = self.extraction_info
except TypeError: # except TypeError:
ext_info = None # ext_info = None
output = { output = {
"id": self.id, "id": self.id,
"plate_number": self.rsl_plate_num, "plate_number": self.rsl_plate_num,
"submission_type": self.submission_type_name, "submission_type": self.client_submission.submission_type_name,
"submitter_plate_number": self.submitter_plate_num, "submitter_plate_number": self.client_submission.submitter_plate_num,
"submitted_date": self.submitted_date.strftime("%Y-%m-%d"), "submitted_date": self.client_submission.submitted_date.strftime("%Y-%m-%d"),
"submitting_lab": sub_lab, "submitting_lab": sub_lab,
"sample_count": self.sample_count, "sample_count": self.client_submission.sample_count,
"extraction_kit": ext_kit, "extraction_kit": "Change submissions.py line 388",
"cost": self.run_cost "cost": self.run_cost
} }
if report: if report:
@@ -433,11 +437,11 @@ class BasicSubmission(BaseClass, LogMixin):
contact_phone = self.contact.phone contact_phone = self.contact.phone
except AttributeError: except AttributeError:
contact_phone = "NA" contact_phone = "NA"
output["submission_category"] = self.submission_category output["submission_category"] = self.client_submission.submission_category
output["technician"] = self.technician output["technician"] = self.technician
output["reagents"] = reagents output["reagents"] = reagents
output["samples"] = samples output["samples"] = samples
output["extraction_info"] = ext_info # output["extraction_info"] = ext_info
output["comment"] = comments output["comment"] = comments
output["equipment"] = equipment output["equipment"] = equipment
output["tips"] = tips output["tips"] = tips
@@ -1236,7 +1240,8 @@ class BasicSubmission(BaseClass, LogMixin):
# # else: # # else:
start_date = cls.rectify_query_date(start_date) start_date = cls.rectify_query_date(start_date)
end_date = cls.rectify_query_date(end_date, eod=True) end_date = cls.rectify_query_date(end_date, eod=True)
query = query.filter(model.submitted_date.between(start_date, end_date)) logger.debug(f"Start date: {start_date}, end date: {end_date}")
query = query.join(ClientSubmission).filter(ClientSubmission.submitted_date.between(start_date, end_date))
# NOTE: by reagent (for some reason) # NOTE: by reagent (for some reason)
match reagent: match reagent:
case str(): case str():
@@ -1256,7 +1261,9 @@ class BasicSubmission(BaseClass, LogMixin):
pass pass
match submission_type_name: match submission_type_name:
case str(): case str():
query = query.filter(model.submission_type_name == submission_type_name) if not start_date:
query = query.join(ClientSubmission)
query = query.filter(ClientSubmission.submission_type_name == submission_type_name)
case _: case _:
pass pass
# NOTE: by id (returns only a single value) # NOTE: by id (returns only a single value)
@@ -1269,7 +1276,7 @@ class BasicSubmission(BaseClass, LogMixin):
limit = 1 limit = 1
case _: case _:
pass pass
query = query.order_by(cls.submitted_date.desc()) # query = query.order_by(cls.submitted_date.desc())
# NOTE: Split query results into pages of size {page_size} # NOTE: Split query results into pages of size {page_size}
if page_size > 0: if page_size > 0:
query = query.limit(page_size) query = query.limit(page_size)
@@ -1471,7 +1478,7 @@ class BasicSubmission(BaseClass, LogMixin):
completed = self.completed_date.date() completed = self.completed_date.date()
except AttributeError: except AttributeError:
completed = None completed = None
return self.calculate_turnaround(start_date=self.submitted_date.date(), end_date=completed) return self.calculate_turnaround(start_date=self.client_submission.submitted_date.date(), end_date=completed)
@classmethod @classmethod
def calculate_turnaround(cls, start_date: date | None = None, end_date: date | None = None) -> int: def calculate_turnaround(cls, start_date: date | None = None, end_date: date | None = None) -> int:
@@ -2459,6 +2466,7 @@ class BasicSample(BaseClass, LogMixin):
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
sample_type = Column(String(32)) #: mode_sub_type of sample sample_type = Column(String(32)) #: mode_sub_type of sample
misc_info = Column(JSON) misc_info = Column(JSON)
control = relationship("Control", back_populates="sample", uselist=False)
sample_submission_associations = relationship( sample_submission_associations = relationship(
"SubmissionSampleAssociation", "SubmissionSampleAssociation",
@@ -2775,140 +2783,140 @@ class BasicSample(BaseClass, LogMixin):
# NOTE: Below are the custom sample types # NOTE: Below are the custom sample types
class WastewaterSample(BasicSample): # class WastewaterSample(BasicSample):
""" # """
Derivative wastewater sample # 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_processing_num = Column(String(64)) #: wastewater processing number
ww_full_sample_id = Column(String(64)) #: full id given by entrics # ww_full_sample_id = Column(String(64)) #: full id given by entrics
rsl_number = Column(String(64)) #: rsl plate identification number # rsl_number = Column(String(64)) #: rsl plate identification number
collection_date = Column(TIMESTAMP) #: Date sample collected # collection_date = Column(TIMESTAMP) #: Date sample collected
received_date = Column(TIMESTAMP) #: Date sample received # received_date = Column(TIMESTAMP) #: Date sample received
notes = Column(String(2000)) #: notes from submission form # notes = Column(String(2000)) #: notes from submission form
sample_location = Column(String(8)) #: location on 24 well plate # sample_location = Column(String(8)) #: location on 24 well plate
__mapper_args__ = dict(polymorphic_identity="Wastewater Sample", # __mapper_args__ = dict(polymorphic_identity="Wastewater Sample",
polymorphic_load="inline", # polymorphic_load="inline",
inherit_condition=(id == BasicSample.id)) # inherit_condition=(id == BasicSample.id))
#
@classmethod # @classmethod
def get_default_info(cls, *args): # def get_default_info(cls, *args):
""" # """
Returns default info for a model. Extends BaseClass method. # Returns default info for a model. Extends BaseClass method.
#
Returns: # Returns:
dict | list | str: Output of key:value dict or single (list, str) desired variable # dict | list | str: Output of key:value dict or single (list, str) desired variable
""" # """
dicto = super().get_default_info(*args) # dicto = super().get_default_info(*args)
match dicto: # match dicto:
case dict(): # case dict():
dicto['singles'] += ['ww_processing_num'] # dicto['singles'] += ['ww_processing_num']
output = {} # output = {}
for k, v in dicto.items(): # for k, v in dicto.items():
if len(args) > 0 and k not in args: # if len(args) > 0 and k not in args:
continue # continue
else: # else:
output[k] = v # output[k] = v
if len(args) == 1: # if len(args) == 1:
return output[args[0]] # return output[args[0]]
case list(): # case list():
if "singles" in args: # if "singles" in args:
dicto += ['ww_processing_num'] # dicto += ['ww_processing_num']
return dicto # return dicto
case _: # case _:
pass # pass
#
def to_sub_dict(self, full_data: bool = False) -> dict: # def to_sub_dict(self, full_data: bool = False) -> dict:
""" # """
gui friendly dictionary, extends parent method. # gui friendly dictionary, extends parent method.
#
Returns: # Returns:
dict: sample id, type, received date, collection date # dict: sample id, type, received date, collection date
""" # """
sample = super().to_sub_dict(full_data=full_data) # sample = super().to_sub_dict(full_data=full_data)
sample['ww_processing_num'] = self.ww_processing_num # sample['ww_processing_num'] = self.ww_processing_num
sample['sample_location'] = self.sample_location # sample['sample_location'] = self.sample_location
sample['received_date'] = self.received_date # sample['received_date'] = self.received_date
sample['collection_date'] = self.collection_date # sample['collection_date'] = self.collection_date
return sample # return sample
#
@classmethod # @classmethod
def parse_sample(cls, input_dict: dict) -> dict: # def parse_sample(cls, input_dict: dict) -> dict:
""" # """
Custom sample parser. Extends parent # Custom sample parser. Extends parent
#
Args: # Args:
input_dict (dict): Basic parser results for this sample. # input_dict (dict): Basic parser results for this sample.
#
Returns: # Returns:
dict: Updated parser results. # dict: Updated parser results.
""" # """
output_dict = super().parse_sample(input_dict) # output_dict = super().parse_sample(input_dict)
disallowed = ["", None, "None"] # disallowed = ["", None, "None"]
try: # try:
check = output_dict['rsl_number'] in disallowed # check = output_dict['rsl_number'] in disallowed
except KeyError: # except KeyError:
check = True # check = True
if check: # if check:
output_dict['rsl_number'] = "RSL-WW-" + output_dict['ww_processing_num'] # output_dict['rsl_number'] = "RSL-WW-" + output_dict['ww_processing_num']
if output_dict['ww_full_sample_id'] is not None and output_dict["submitter_id"] in disallowed: # if output_dict['ww_full_sample_id'] is not None and output_dict["submitter_id"] in disallowed:
output_dict["submitter_id"] = output_dict['ww_full_sample_id'] # output_dict["submitter_id"] = output_dict['ww_full_sample_id']
# check = check_key_or_attr("rsl_number", output_dict, check_none=True) # # check = check_key_or_attr("rsl_number", output_dict, check_none=True)
return output_dict # return output_dict
#
@classproperty # @classproperty
def searchables(cls) -> List[dict]: # def searchables(cls) -> List[dict]:
""" # """
Delivers a list of fields that can be used in fuzzy search. Extends parent. # Delivers a list of fields that can be used in fuzzy search. Extends parent.
#
Returns: # Returns:
List[str]: List of fields. # List[str]: List of fields.
""" # """
searchables = deepcopy(super().searchables) # searchables = deepcopy(super().searchables)
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]: # for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title() # label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
searchables.append(dict(label=label, field=item)) # searchables.append(dict(label=label, field=item))
return searchables # return searchables
#
#
class BacterialCultureSample(BasicSample): # class BacterialCultureSample(BasicSample):
""" # """
base of bacterial culture sample # 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 # organism = Column(String(64)) #: bacterial specimen
concentration = Column(String(16)) #: sample concentration # concentration = Column(String(16)) #: sample concentration
control = relationship("IridaControl", back_populates="sample", uselist=False) # control = relationship("IridaControl", back_populates="sample", uselist=False)
__mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample", # __mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample",
polymorphic_load="inline", # polymorphic_load="inline",
inherit_condition=(id == BasicSample.id)) # inherit_condition=(id == BasicSample.id))
#
def to_sub_dict(self, full_data: bool = False) -> dict: # def to_sub_dict(self, full_data: bool = False) -> dict:
""" # """
gui friendly dictionary, extends parent method. # gui friendly dictionary, extends parent method.
#
Returns: # Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above # dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
""" # """
sample = super().to_sub_dict(full_data=full_data) # sample = super().to_sub_dict(full_data=full_data)
sample['name'] = self.submitter_id # sample['name'] = self.submitter_id
sample['organism'] = self.organism # sample['organism'] = self.organism
try: # try:
sample['concentration'] = f"{float(self.concentration):.2f}" # sample['concentration'] = f"{float(self.concentration):.2f}"
except (TypeError, ValueError): # except (TypeError, ValueError):
sample['concentration'] = 0.0 # sample['concentration'] = 0.0
if self.control is not None: # if self.control is not None:
sample['colour'] = [0, 128, 0] # sample['colour'] = [0, 128, 0]
target = next((v for k, v in self.control.controltype.targets.items() if k == self.control.subtype), # target = next((v for k, v in self.control.controltype.targets.items() if k == self.control.subtype),
"Not Available") # "Not Available")
try: # try:
target = ", ".join(target) # target = ", ".join(target)
except: # except:
target = "None" # target = "None"
sample['tooltip'] = f"\nControl: {self.control.controltype.name} - {target}" # sample['tooltip'] = f"\nControl: {self.control.controltype.name} - {target}"
return sample # return sample
#
# # NOTE: Submission to Sample Associations # # NOTE: Submission to Sample Associations
@@ -2920,26 +2928,27 @@ class SubmissionSampleAssociation(BaseClass):
id = Column(INTEGER, unique=True, nullable=False) #: id to be used for inheriting purposes id = Column(INTEGER, unique=True, nullable=False) #: id to be used for inheriting purposes
sample_id = Column(INTEGER, ForeignKey("_basicsample.id"), nullable=False) #: id of associated sample sample_id = Column(INTEGER, ForeignKey("_basicsample.id"), nullable=False) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id"), primary_key=True) #: id of associated submission submission_id = Column(INTEGER, ForeignKey("_clientsubmission.id"), primary_key=True) #: id of associated submission
row = Column(INTEGER, primary_key=True) #: row on the 96 well plate row = Column(INTEGER, primary_key=True) #: row on the 96 well plate
column = Column(INTEGER, primary_key=True) #: column on the 96 well plate column = Column(INTEGER, primary_key=True) #: column on the 96 well plate
submission_rank = Column(INTEGER, nullable=False, default=0) #: Location in sample list submission_rank = Column(INTEGER, nullable=False, default=0) #: Location in sample list
misc_info = Column(JSON)
# NOTE: reference to the Submission object # NOTE: reference to the Submission object
submission = relationship(BasicSubmission, submission = relationship(ClientSubmission,
back_populates="submission_sample_associations") #: associated submission back_populates="submission_sample_associations") #: associated submission
# NOTE: reference to the Sample object # NOTE: reference to the Sample object
sample = relationship(BasicSample, back_populates="sample_submission_associations") #: associated sample sample = relationship(BasicSample, back_populates="sample_submission_associations") #: associated sample
base_sub_type = Column(String) #: string of mode_sub_type name # base_sub_type = Column(String) #: string of mode_sub_type name
# NOTE: Refers to the type of parent. # NOTE: Refers to the type of parent.
__mapper_args__ = { # __mapper_args__ = {
"polymorphic_identity": "Basic Association", # "polymorphic_identity": "Basic Association",
"polymorphic_on": base_sub_type, # "polymorphic_on": base_sub_type,
"with_polymorphic": "*", # "with_polymorphic": "*",
} # }
def __init__(self, submission: BasicSubmission = None, sample: BasicSample = None, row: int = 1, column: int = 1, def __init__(self, submission: BasicSubmission = None, sample: BasicSample = None, row: int = 1, column: int = 1,
id: int | None = None, submission_rank: int = 0, **kwargs): id: int | None = None, submission_rank: int = 0, **kwargs):
@@ -3183,96 +3192,96 @@ class SubmissionSampleAssociation(BaseClass):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")
class WastewaterAssociation(SubmissionSampleAssociation): # class WastewaterAssociation(SubmissionSampleAssociation):
""" # """
table containing wastewater specific submission/sample associations # table containing wastewater specific submission/sample associations
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html # DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" # """
id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True) # id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True)
ct_n1 = Column(FLOAT(2)) #: AKA ct for N1 # ct_n1 = Column(FLOAT(2)) #: AKA ct for N1
ct_n2 = Column(FLOAT(2)) #: AKA ct for N2 # ct_n2 = Column(FLOAT(2)) #: AKA ct for N2
n1_status = Column(String(32)) #: positive or negative for N1 # n1_status = Column(String(32)) #: positive or negative for N1
n2_status = Column(String(32)) #: positive or negative for N2 # n2_status = Column(String(32)) #: positive or negative for N2
pcr_results = Column(JSON) #: imported PCR status from QuantStudio # pcr_results = Column(JSON) #: imported PCR status from QuantStudio
#
__mapper_args__ = dict(polymorphic_identity="Wastewater Association", # __mapper_args__ = dict(polymorphic_identity="Wastewater Association",
polymorphic_load="inline", # polymorphic_load="inline",
inherit_condition=(id == SubmissionSampleAssociation.id)) # inherit_condition=(id == SubmissionSampleAssociation.id))
#
def to_sub_dict(self) -> dict: # def to_sub_dict(self) -> dict:
""" # """
Returns a sample dictionary updated with instance information. Extends parent # Returns a sample dictionary updated with instance information. Extends parent
#
Returns: # Returns:
dict: Updated dictionary with row, column and well updated # dict: Updated dictionary with row, column and well updated
""" # """
#
sample = super().to_sub_dict() # sample = super().to_sub_dict()
sample['ct'] = f"({self.ct_n1}, {self.ct_n2})" # sample['ct'] = f"({self.ct_n1}, {self.ct_n2})"
try: # try:
sample['source_row'] = row_keys[self.sample.sample_location[0]] # sample['source_row'] = row_keys[self.sample.sample_location[0]]
sample['source_column'] = int(self.sample.sample_location[1:]) # sample['source_column'] = int(self.sample.sample_location[1:])
except (TypeError, AttributeError) as e: # except (TypeError, AttributeError) as e:
logger.error(f"Couldn't set sources for {self.sample.rsl_number}. Looks like there isn't data.") # logger.error(f"Couldn't set sources for {self.sample.rsl_number}. Looks like there isn't data.")
try: # try:
sample['positive'] = any(["positive" in item for item in [self.n1_status, self.n2_status]]) # sample['positive'] = any(["positive" in item for item in [self.n1_status, self.n2_status]])
except (TypeError, AttributeError) as e: # except (TypeError, AttributeError) as e:
logger.error(f"Couldn't check positives for {self.sample.rsl_number}. Looks like there isn't PCR data.") # logger.error(f"Couldn't check positives for {self.sample.rsl_number}. Looks like there isn't PCR data.")
return sample # return sample
#
@property # @property
def hitpicked(self) -> dict | None: # def hitpicked(self) -> dict | None:
""" # """
Outputs a dictionary usable for html plate maps. Extends parent # Outputs a dictionary usable for html plate maps. Extends parent
#
Returns: # Returns:
dict: dictionary of sample id, row and column in elution plate # dict: dictionary of sample id, row and column in elution plate
""" # """
sample = super().hitpicked # sample = super().hitpicked
try: # try:
scaler = max([self.ct_n1, self.ct_n2]) # scaler = max([self.ct_n1, self.ct_n2])
except TypeError: # except TypeError:
scaler = 0.0 # scaler = 0.0
if scaler == 0.0: # if scaler == 0.0:
scaler = 45 # scaler = 45
bg = (45 - scaler) * 17 # bg = (45 - scaler) * 17
red = min([64 + bg, 255]) # red = min([64 + bg, 255])
grn = max([255 - bg, 0]) # grn = max([255 - bg, 0])
blu = 128 # blu = 128
sample['background_color'] = f"rgb({red}, {grn}, {blu})" # sample['background_color'] = f"rgb({red}, {grn}, {blu})"
try: # try:
sample[ # sample[
'tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)}<br>- ct N2: {'{:.2f}'.format(self.ct_n2)}" # 'tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)}<br>- ct N2: {'{:.2f}'.format(self.ct_n2)}"
except (TypeError, AttributeError) as e: # except (TypeError, AttributeError) as e:
logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.") # logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.")
return sample # return sample
#
#
class WastewaterArticAssociation(SubmissionSampleAssociation): # class WastewaterArticAssociation(SubmissionSampleAssociation):
""" # """
table containing wastewater artic specific submission/sample associations # table containing wastewater artic specific submission/sample associations
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html # DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" # """
id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True) # id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True)
source_plate = Column(String(32)) # source_plate = Column(String(32))
source_plate_number = Column(INTEGER) # source_plate_number = Column(INTEGER)
source_well = Column(String(8)) # source_well = Column(String(8))
ct = Column(String(8)) #: AKA ct for N1 # ct = Column(String(8)) #: AKA ct for N1
#
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic Association", # __mapper_args__ = dict(polymorphic_identity="Wastewater Artic Association",
polymorphic_load="inline", # polymorphic_load="inline",
inherit_condition=(id == SubmissionSampleAssociation.id)) # inherit_condition=(id == SubmissionSampleAssociation.id))
#
def to_sub_dict(self) -> dict: # def to_sub_dict(self) -> dict:
""" # """
Returns a sample dictionary updated with instance information. Extends parent # Returns a sample dictionary updated with instance information. Extends parent
#
Returns: # Returns:
dict: Updated dictionary with row, column and well updated # dict: Updated dictionary with row, column and well updated
""" # """
sample = super().to_sub_dict() # sample = super().to_sub_dict()
sample['ct'] = self.ct # sample['ct'] = self.ct
sample['source_plate'] = self.source_plate # sample['source_plate'] = self.source_plate
sample['source_plate_number'] = self.source_plate_number # sample['source_plate_number'] = self.source_plate_number
sample['source_well'] = self.source_well # sample['source_well'] = self.source_well
return sample # return sample

View File

@@ -47,7 +47,7 @@ class ReportMaker(object):
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date, page_size=0) self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date, page_size=0)
if organizations is not None: if organizations is not None:
self.subs = [sub for sub in self.subs if sub.submitting_lab.name in organizations] self.subs = [sub for sub in self.subs if sub.client_submission.submitting_lab.name in organizations]
self.detailed_df, self.summary_df = self.make_report_xlsx() self.detailed_df, self.summary_df = self.make_report_xlsx()
self.html = self.make_report_html(df=self.summary_df) self.html = self.make_report_html(df=self.summary_df)
@@ -183,7 +183,10 @@ class TurnaroundMaker(ReportArchetype):
except (AttributeError, KeyError): except (AttributeError, KeyError):
tat = None tat = None
if not tat: if not tat:
tat = ctx.TaT_threshold try:
tat = ctx.TaT_threshold
except AttributeError:
tat = 3
try: try:
tat_ok = days <= tat tat_ok = days <= tat
except TypeError: except TypeError:

View File

@@ -12,7 +12,7 @@ import logging, numpy as np
from pprint import pformat from pprint import pformat
from typing import Tuple, List from typing import Tuple, List
from pathlib import Path from pathlib import Path
from backend.db.models import WastewaterArtic from backend.db.models import BasicSubmission
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,7 +20,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
# Main window class # Main window class
class GelBox(QDialog): class GelBox(QDialog):
def __init__(self, parent, img_path: str | Path, submission: WastewaterArtic): def __init__(self, parent, img_path: str | Path, submission: BasicSubmission):
super().__init__(parent) super().__init__(parent)
# NOTE: setting title # NOTE: setting title
self.setWindowTitle(f"Gel - {img_path}") self.setWindowTitle(f"Gel - {img_path}")

View File

@@ -1195,36 +1195,48 @@ class Settings(BaseSettings, extra="allow"):
func = function[1] func = function[1]
# NOTE: assign function based on its name being in config: startup/teardown # NOTE: assign function based on its name being in config: startup/teardown
# NOTE: scripts must be registered using {name: Null} in the database # NOTE: scripts must be registered using {name: Null} in the database
if name in self.startup_scripts.keys(): try:
self.startup_scripts[name] = func if name in self.startup_scripts.keys():
if name in self.teardown_scripts.keys(): self.startup_scripts[name] = func
self.teardown_scripts[name] = func except AttributeError:
pass
try:
if name in self.teardown_scripts.keys():
self.teardown_scripts[name] = func
except AttributeError:
pass
@timer @timer
def run_startup(self): def run_startup(self):
""" """
Runs startup scripts. Runs startup scripts.
""" """
for script in self.startup_scripts.values(): try:
try: for script in self.startup_scripts.values():
logger.info(f"Running startup script: {script.__name__}") try:
thread = Thread(target=script, args=(ctx,)) logger.info(f"Running startup script: {script.__name__}")
thread.start() thread = Thread(target=script, args=(ctx,))
except AttributeError: thread.start()
logger.error(f"Couldn't run startup script: {script}") except AttributeError:
logger.error(f"Couldn't run startup script: {script}")
except AttributeError:
pass
@timer @timer
def run_teardown(self): def run_teardown(self):
""" """
Runs teardown scripts. Runs teardown scripts.
""" """
for script in self.teardown_scripts.values(): try:
try: for script in self.teardown_scripts.values():
logger.info(f"Running teardown script: {script.__name__}") try:
thread = Thread(target=script, args=(ctx,)) logger.info(f"Running teardown script: {script.__name__}")
thread.start() thread = Thread(target=script, args=(ctx,))
except AttributeError: thread.start()
logger.error(f"Couldn't run teardown script: {script}") except AttributeError:
logger.error(f"Couldn't run teardown script: {script}")
except AttributeError:
pass
@classmethod @classmethod
def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str: def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str: