Overhauling database.

This commit is contained in:
lwark
2025-05-13 13:20:01 -05:00
parent 0dbb4ae77a
commit 75c665ea05
21 changed files with 729 additions and 1727 deletions

View File

@@ -27,7 +27,7 @@ def import_irida(ctx: Settings):
except AttributeError as e: except AttributeError as e:
logger.error(f"Error, could not import from irida due to {e}") logger.error(f"Error, could not import from irida due to {e}")
return return
sql = "SELECT name, submitted_date, submission_id, contains, matches, kraken, subtype, refseq_version, " \ sql = "SELECT name, submitted_date, run_id, contains, matches, kraken, subtype, refseq_version, " \
"kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \ "kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \
f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})" f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})"
cursor = conn.execute(sql) cursor = conn.execute(sql)
@@ -57,7 +57,7 @@ def import_irida(ctx: Settings):
except IndexError: except IndexError:
logger.error(f"Could not get sample for {sample}") logger.error(f"Could not get sample for {sample}")
instance.submission = None instance.submission = None
# instance.submission = sample.submission[0] # instance.run = sample.run[0]
new_session.add(instance) new_session.add(instance)
new_session.commit() new_session.commit()
new_session.close() new_session.close()

View File

@@ -33,11 +33,13 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns __table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
singles = ['id'] singles = ['id']
omni_removes = ["id", 'submissions', "omnigui_class_dict", "omnigui_instance_dict"] omni_removes = ["id", 'runs', "omnigui_class_dict", "omnigui_instance_dict"]
omni_sort = ["name"] omni_sort = ["name"]
omni_inheritable = [] omni_inheritable = []
searchables = [] searchables = []
misc_info = Column(JSON)
def __repr__(self) -> str: def __repr__(self) -> str:
try: try:
return f"<{self.__class__.__name__}({self.name})>" return f"<{self.__class__.__name__}({self.name})>"
@@ -120,6 +122,26 @@ class BaseClass(Base):
from test_settings import ctx from test_settings import ctx
return ctx.backup_path return ctx.backup_path
@classproperty
def jsons(cls) -> List[str]:
"""
Get list of JSON db columns
Returns:
List[str]: List of column names
"""
return [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
@classproperty
def timestamps(cls) -> List[str]:
"""
Get list of TIMESTAMP columns
Returns:
List[str]: List of column names
"""
return [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
@classmethod @classmethod
def get_default_info(cls, *args) -> dict | list | str: def get_default_info(cls, *args) -> dict | list | str:
""" """
@@ -150,7 +172,6 @@ class BaseClass(Base):
else: else:
return cls.__subclasses__() return cls.__subclasses__()
@classmethod @classmethod
def fuzzy_search(cls, **kwargs) -> List[Any]: def fuzzy_search(cls, **kwargs) -> List[Any]:
""" """
@@ -177,7 +198,7 @@ class BaseClass(Base):
@classmethod @classmethod
def results_to_df(cls, objects: list | None = None, **kwargs) -> DataFrame: def results_to_df(cls, objects: list | None = None, **kwargs) -> DataFrame:
""" """
Converts class sub_dicts into a Dataframe for all instances of the class. Converts class sub_dicts into a Dataframe for all controls of the class.
Args: Args:
objects (list): Objects to be converted to dataframe. objects (list): Objects to be converted to dataframe.
@@ -519,12 +540,13 @@ class ConfigItem(BaseClass):
from .controls import * from .controls import *
# NOTE: import order must go: orgs, kit, subs due to circular import issues # NOTE: import order must go: orgs, kit, runs due to circular import issues
from .organizations import * from .organizations import *
from .runs import *
from .kits import * from .kits import *
from .submissions import * from .submissions import *
from .audit import AuditLog from .audit import AuditLog
# NOTE: Add a creator to the submission for reagent association. Assigned here due to circular import constraints. # NOTE: Add a creator to the run for reagent association. Assigned here due to circular import constraints.
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator # https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator
BasicSubmission.reagents.creator = lambda reg: SubmissionReagentAssociation(reagent=reg) Procedure.reagents.creator = lambda reg: ProcedureReagentAssociation(reagent=reg)

View File

@@ -27,7 +27,7 @@ class ControlType(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. Irida Control) name = Column(String(255), unique=True) #: controltype name (e.g. Irida Control)
targets = Column(JSON) #: organisms checked for targets = Column(JSON) #: organisms checked for
instances = relationship("Control", back_populates="controltype") #: control samples created of this type. controls = relationship("Control", back_populates="controltype") #: control samples created of this type.
@classmethod @classmethod
@setup_lookup @setup_lookup
@@ -64,11 +64,11 @@ class ControlType(BaseClass):
Returns: Returns:
List[str]: list of subtypes available List[str]: list of subtypes available
""" """
if not self.instances: if not self.controls:
return return
# NOTE: Get first instance since all should have same subtypes # NOTE: Get first instance since all should have same subtypes
# NOTE: Get mode of instance # NOTE: Get mode of instance
jsoner = getattr(self.instances[0], mode) jsoner = getattr(self.controls[0], mode)
try: try:
# NOTE: Pick genera (all should have same subtypes) # NOTE: Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0] genera = list(jsoner.keys())[0]
@@ -119,17 +119,17 @@ class Control(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
controltype_name = Column(String, ForeignKey("_controltype.name", ondelete="SET NULL", controltype_name = Column(String, ForeignKey("_controltype.name", ondelete="SET NULL",
name="fk_BC_subtype_name")) #: name of joined submission type name="fk_BC_subtype_name")) #: name of joined run type
controltype = relationship("ControlType", back_populates="instances", controltype = relationship("ControlType", back_populates="controls",
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", sample_id = Column(String, ForeignKey("_basicsample.id", ondelete="SET NULL",
name="fk_Cont_sample_id")) #: name of joined submission type name="fk_Cont_sample_id")) #: name of joined run type
sample = relationship("BasicSample", back_populates="control") #: This control's submission sample sample = relationship("BasicSample", back_populates="control") #: This control's run 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 procedure_id = Column(INTEGER, ForeignKey("_procedure.id")) #: parent run id
submission = relationship("BasicSubmission", back_populates="controls", procedure = relationship("Procedure", back_populates="controls",
foreign_keys=[submission_id]) #: parent submission foreign_keys=[procedure_id]) #: parent run
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "Basic Control", "polymorphic_identity": "Basic Control",
@@ -147,7 +147,7 @@ class Control(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submissiontype: str | None = None, proceduretype: str | None = None,
subtype: str | None = None, subtype: str | None = None,
start_date: date | datetime | str | int | None = None, start_date: date | datetime | str | int | None = None,
end_date: date | datetime | str | int | None = None, end_date: date | datetime | str | int | None = None,
@@ -158,7 +158,7 @@ class Control(BaseClass):
Lookup control objects in the database based on a number of parameters. Lookup control objects in the database based on a number of parameters.
Args: Args:
submissiontype (str | None, optional): Submission type associated with control. Defaults to None. proceduretype (str | None, optional): Submission type associated with control. Defaults to None.
subtype (str | None, optional): Control subtype, eg IridaControl. Defaults to None. subtype (str | None, optional): Control subtype, eg IridaControl. Defaults to None.
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None. start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None. end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
@@ -168,15 +168,15 @@ class Control(BaseClass):
Returns: Returns:
Control|List[Control]: Control object of interest. Control|List[Control]: Control object of interest.
""" """
from backend.db import SubmissionType from backend.db import ProcedureType
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match submissiontype: match proceduretype:
case str(): case str():
from backend import BasicSubmission, SubmissionType from backend.db import Procedure
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submissiontype) query = query.join(Procedure).join(ProcedureType).filter(ProcedureType.name == proceduretype)
case SubmissionType(): case ProcedureType():
from backend import BasicSubmission from backend import Procedure
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submissiontype.name) query = query.join(Procedure).filter(Procedure.submission_type_name == proceduretype.name)
case _: case _:
pass pass
# NOTE: by control type # NOTE: by control type
@@ -234,13 +234,13 @@ class Control(BaseClass):
model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_ model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicSubmission") f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicRun")
case ControlType(): case ControlType():
try: try:
model = cls.__mapper__.polymorphic_map[polymorphic_identity.name].class_ model = cls.__mapper__.polymorphic_map[polymorphic_identity.name].class_
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicSubmission") f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicRun")
case _: case _:
pass pass
# NOTE: if attrs passed in and this cls doesn't have all attributes in attr # NOTE: if attrs passed in and this cls doesn't have all attributes in attr
@@ -335,7 +335,7 @@ class PCRControl(Control):
parent.mode_typer.clear() parent.mode_typer.clear()
parent.mode_typer.setEnabled(False) parent.mode_typer.setEnabled(False)
report = Report() report = Report()
controls = cls.query(submissiontype=chart_settings['sub_type'], start_date=chart_settings['start_date'], controls = cls.query(proceduretype=chart_settings['sub_type'], start_date=chart_settings['start_date'],
end_date=chart_settings['end_date']) end_date=chart_settings['end_date'])
data = [control.to_sub_dict() for control in controls] data = [control.to_sub_dict() for control in controls]
df = DataFrame.from_records(data) df = DataFrame.from_records(data)
@@ -402,7 +402,7 @@ class IridaControl(Control):
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
""" """
Converts object into convenient dictionary for use in submission summary Converts object into convenient dictionary for use in run summary
Returns: Returns:
dict: output dictionary containing: Name, Type, Targets, Top Kraken results dict: output dictionary containing: Name, Type, Targets, Top Kraken results
@@ -565,7 +565,7 @@ class IridaControl(Control):
Args: Args:
input_df (list[dict]): list of dictionaries containing records input_df (list[dict]): list of dictionaries containing records
sub_mode (str | None, optional): sub_type of submission type. Defaults to None. sub_mode (str | None, optional): sub_type of run type. Defaults to None.
Returns: Returns:
DataFrame: dataframe of controls DataFrame: dataframe of controls

View File

@@ -17,7 +17,7 @@ from tools import check_authorization, setup_lookup, Report, Result, check_regex
from typing import List, Literal, Generator, Any, Tuple from typing import List, Literal, Generator, Any, Tuple
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path from pathlib import Path
from . import Base, BaseClass, Organization, LogMixin from . import Base, BaseClass, Organization, LogMixin, ProcedureType
from io import BytesIO from io import BytesIO
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
@@ -94,10 +94,10 @@ equipment_tips = Table(
extend_existing=True extend_existing=True
) )
kittypes_submissions = Table( kittypes_runs = Table(
"_kittypes_submissions", "_kittypes_runs",
Base.metadata, Base.metadata,
Column("_basicsubmission_id", INTEGER, ForeignKey("_basicsubmission.id")), Column("_basicrun_id", INTEGER, ForeignKey("_basicrun.id")),
Column("kittype_id", INTEGER, ForeignKey("_kittype.id")), Column("kittype_id", INTEGER, ForeignKey("_kittype.id")),
extend_existing=True extend_existing=True
) )
@@ -105,15 +105,15 @@ kittypes_submissions = Table(
class KitType(BaseClass): class KitType(BaseClass):
""" """
Base of kits used in submission processing Base of kits used in run processing
""" """
omni_sort = BaseClass.omni_sort + ["kit_submissiontype_associations", "kit_reagentrole_associations", "processes"] omni_sort = BaseClass.omni_sort + ["kit_submissiontype_associations", "kit_reagentrole_associations", "processes"]
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="kittypes", runs = relationship("BasicRun", back_populates="kittypes",
secondary=kittypes_submissions) #: submissions this kit was used for secondary=kittypes_runs) #: runs this kit was used for
processes = relationship("Process", back_populates="kit_types", processes = relationship("Process", back_populates="kit_types",
secondary=kittypes_processes) #: equipment processes used by this kit secondary=kittypes_processes) #: equipment processes used by this kit
@@ -204,7 +204,7 @@ class KitType(BaseClass):
# logger.debug(f"Submission type: {submission_type}, Kit: {self}") # logger.debug(f"Submission type: {submission_type}, Kit: {self}")
assocs = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type] assocs = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type]
# logger.debug(f"Associations: {assocs}") # logger.debug(f"Associations: {assocs}")
# NOTE: rescue with submission type's default kit. # NOTE: rescue with run type's default kit.
if not assocs: if not assocs:
logger.error( logger.error(
f"No associations found with {self}. Attempting rescue with default kit: {submission_type.default_kit}") f"No associations found with {self}. Attempting rescue with default kit: {submission_type.default_kit}")
@@ -213,7 +213,7 @@ class KitType(BaseClass):
from frontend.widgets.pop_ups import ObjectSelector from frontend.widgets.pop_ups import ObjectSelector
dlg = ObjectSelector( dlg = ObjectSelector(
title="Select Kit", title="Select Kit",
message="Could not find reagents for this submission type/kit type combo.\nSelect new kit.", message="Could not find reagents for this run type/kit type combo.\nSelect new kit.",
obj_type=self.__class__, obj_type=self.__class__,
values=[kit.name for kit in submission_type.kit_types] values=[kit.name for kit in submission_type.kit_types]
) )
@@ -378,7 +378,7 @@ class KitType(BaseClass):
# if not new_role: # if not new_role:
# new_role = EquipmentRole(name=role['role']) # new_role = EquipmentRole(name=role['role'])
# for equipment in Equipment.assign_equipment(equipment_role=new_role): # for equipment in Equipment.assign_equipment(equipment_role=new_role):
# new_role.instances.append(equipment) # new_role.controls.append(equipment)
# ster_assoc = SubmissionTypeEquipmentRoleAssociation(submission_type=submission_type, # ster_assoc = SubmissionTypeEquipmentRoleAssociation(submission_type=submission_type,
# equipment_role=new_role) # equipment_role=new_role)
# try: # try:
@@ -425,7 +425,7 @@ class ReagentRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of role reagent plays name = Column(String(64)) #: name of role reagent plays
instances = relationship("Reagent", back_populates="role", instances = relationship("Reagent", back_populates="role",
secondary=reagentroles_reagents) #: concrete instances of this reagent type secondary=reagentroles_reagents) #: concrete controls of this reagent type
eol_ext = Column(Interval()) #: extension of life interval eol_ext = Column(Interval()) #: extension of life interval
reagentrole_kit_associations = relationship( reagentrole_kit_associations = relationship(
@@ -548,7 +548,7 @@ class Reagent(BaseClass, LogMixin):
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("ReagentRole", back_populates="instances", role = relationship("ReagentRole", back_populates="controls",
secondary=reagentroles_reagents) #: joined parent reagent type secondary=reagentroles_reagents) #: joined parent reagent type
role_id = Column(INTEGER, ForeignKey("_reagentrole.id", ondelete='SET NULL', role_id = Column(INTEGER, ForeignKey("_reagentrole.id", ondelete='SET NULL',
name="fk_reagent_role_id")) #: id of parent reagent type name="fk_reagent_role_id")) #: id of parent reagent type
@@ -557,13 +557,13 @@ class Reagent(BaseClass, LogMixin):
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
reagent_submission_associations = relationship( reagent_submission_associations = relationship(
"SubmissionReagentAssociation", "RunReagentAssociation",
back_populates="reagent", back_populates="reagent",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) #: Relation to SubmissionSampleAssociation ) #: Relation to SubmissionSampleAssociation
submissions = association_proxy("reagent_submission_associations", "submission", submissions = association_proxy("reagent_submission_associations", "run",
creator=lambda sub: SubmissionReagentAssociation( creator=lambda sub: RunReagentAssociation(
submission=sub)) #: Association proxy to SubmissionSampleAssociation.samples submission=sub)) #: Association proxy to SubmissionSampleAssociation.samples
def __repr__(self): def __repr__(self):
@@ -845,10 +845,10 @@ class SubmissionType(BaseClass):
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(128), unique=True) #: name of submission type name = Column(String(128), unique=True) #: name of run 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 run type
instances = relationship("ClientSubmission", back_populates="submission_type") #: Concrete instances of this type. clientsubmissions = relationship("ClientSubmission", back_populates="submission_type") #: Concrete controls 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.
@@ -1107,7 +1107,7 @@ class SubmissionType(BaseClass):
@classproperty @classproperty
def omni_removes(cls): def omni_removes(cls):
return super().omni_removes + ["defaults", "instances"] return super().omni_removes + ["defaults", "controls"]
@classproperty @classproperty
def basic_template(cls) -> bytes: def basic_template(cls) -> bytes:
@@ -1243,15 +1243,15 @@ class SubmissionType(BaseClass):
return list(set([item for items in relevant for item in items if item is not None])) return list(set([item for items in relevant for item in items if item is not None]))
@property @property
def submission_class(self) -> "BasicSubmission": def submission_class(self) -> "BasicRun":
""" """
Gets submission class associated with this submission type. Gets run class associated with this run type.
Returns: Returns:
BasicSubmission: Submission class BasicRun: Submission class
""" """
from .submissions import BasicSubmission from .submissions import BasicRun
return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name) return BasicRun.find_polymorphic_subclass(polymorphic_identity=self.name)
@classmethod @classmethod
def query_or_create(cls, **kwargs) -> Tuple[SubmissionType, bool]: def query_or_create(cls, **kwargs) -> Tuple[SubmissionType, bool]:
@@ -1264,7 +1264,7 @@ class SubmissionType(BaseClass):
new = True new = True
for k, v in sanitized_kwargs.items(): for k, v in sanitized_kwargs.items():
setattr(instance, k, v) setattr(instance, k, v)
logger.info(f"Instance from submissiontype query or create: {instance}") logger.info(f"Instance from proceduretype query or create: {instance}")
return instance, new return instance, new
@classmethod @classmethod
@@ -1276,10 +1276,10 @@ class SubmissionType(BaseClass):
**kwargs **kwargs
) -> SubmissionType | List[SubmissionType]: ) -> SubmissionType | List[SubmissionType]:
""" """
Lookup submission type in the database by a number of parameters Lookup run type in the database by a number of parameters
Args: Args:
name (str | None, optional): Name of submission type. Defaults to None. name (str | None, optional): Name of run type. Defaults to None.
key (str | None, optional): A key present in the info-map to lookup. Defaults to None. key (str | None, optional): A key present in the info-map to lookup. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -1319,7 +1319,7 @@ class SubmissionType(BaseClass):
@check_authorization @check_authorization
def save(self): def save(self):
""" """
Adds this instances to the database and commits. Adds this controls to the database and commits.
""" """
super().save() super().save()
@@ -1399,29 +1399,29 @@ class SubmissionType(BaseClass):
return dicto return dicto
class SubmissionTypeKitTypeAssociation(BaseClass): class ProcedureTypeKitTypeAssociation(BaseClass):
""" """
Abstract of relationship between kits and their submission type. Abstract of relationship between kits and their run type.
""" """
omni_removes = BaseClass.omni_removes + ["submission_types_id", "kits_id"] omni_removes = BaseClass.omni_removes + ["procedure_type_id", "procedure_id"]
omni_sort = ["submission_type", "kit_type"] omni_sort = ["proceduretype", "kittype"]
level = 2 level = 2
submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"), proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
primary_key=True) #: id of joined submission type primary_key=True) #: id of joined run type
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
mutable_cost_column = Column( mutable_cost_column = Column(
FLOAT(2)) #: dollar amount per 96 well plate that can change with number of columns (reagents, tips, etc) FLOAT(2)) #: dollar amount per 96 well plate that can change with number of columns (reagents, tips, etc)
mutable_cost_sample = Column( mutable_cost_sample = Column(
FLOAT(2)) #: dollar amount that can change with number of samples (reagents, tips, etc) FLOAT(2)) #: dollar amount that can change with number of samples (reagents, tips, etc)
constant_cost = Column(FLOAT(2)) #: dollar amount per plate that will remain constant (plates, man hours, etc) constant_cost = Column(FLOAT(2)) #: dollar amount per plate that will remain constant (plates, man hours, etc)
kit_type = relationship(KitType, back_populates="kit_submissiontype_associations") #: joined kittype kittype = relationship(KitType, back_populates="kittype_proceduretype_associations") #: joined kittype
# reference to the "SubmissionType" object # reference to the "SubmissionType" object
submission_type = relationship(SubmissionType, proceduretype = relationship(ProcedureType,
back_populates="submissiontype_kit_associations") #: joined submission type back_populates="proceduretype_kittype_associations") #: joined run type
def __init__(self, kit_type=None, submission_type=None, def __init__(self, kit_type=None, submission_type=None,
mutable_cost_column: int = 0.00, mutable_cost_sample: int = 0.00, constant_cost: int = 0.00): mutable_cost_column: int = 0.00, mutable_cost_sample: int = 0.00, constant_cost: int = 0.00):
@@ -1495,7 +1495,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
Lookup SubmissionTypeKitTypeAssociations of interest. Lookup SubmissionTypeKitTypeAssociations of interest.
Args: Args:
submission_type (SubmissionType | str | int | None, optional): Identifier of submission type. Defaults to None. submission_type (SubmissionType | str | int | None, optional): Identifier of run type. Defaults to None.
kit_type (KitType | str | int | None, optional): Identifier of kit type. Defaults to None. kit_type (KitType | str | int | None, optional): Identifier of kit type. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -1572,7 +1572,7 @@ class KitTypeReagentRoleAssociation(BaseClass):
primary_key=True) #: id of associated reagent type primary_key=True) #: id of associated reagent type
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type
submission_type_id = Column(INTEGER, ForeignKey("_submissiontype.id"), primary_key=True) submission_type_id = Column(INTEGER, ForeignKey("_submissiontype.id"), primary_key=True)
uses = Column(JSON) #: map to location on excel sheets of different submission types uses = Column(JSON) #: map to location on excel sheets of different run types
required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0) 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 last_used = Column(String(32)) #: last used lot number of this type of reagent
@@ -1677,7 +1677,7 @@ class KitTypeReagentRoleAssociation(BaseClass):
v = KitType.query(name=v) v = KitType.query(name=v)
else: else:
v = v.instance_object v = v.instance_object
case "submissiontype" | "submission_type": case "proceduretype" | "submission_type":
k = "submission_type" k = "submission_type"
if isinstance(v, str): if isinstance(v, str):
v = SubmissionType.query(name=v) v = SubmissionType.query(name=v)
@@ -1742,18 +1742,6 @@ class KitTypeReagentRoleAssociation(BaseClass):
limit = 1 limit = 1
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
# def to_export_dict(self) -> dict:
# """
# Creates a dictionary of relevant values in this object.
#
# Returns:
# dict: dictionary of Association and related reagent role
# """
# base_dict = dict(required=self.required)
# for k, v in self.reagent_role.to_export_dict().items():
# base_dict[k] = v
# return base_dict
def get_all_relevant_reagents(self) -> Generator[Reagent, None, None]: def get_all_relevant_reagents(self) -> Generator[Reagent, None, None]:
""" """
Creates a generator that will resolve in to a list filling the role associated with this object. Creates a generator that will resolve in to a list filling the role associated with this object.
@@ -1761,7 +1749,7 @@ class KitTypeReagentRoleAssociation(BaseClass):
Returns: Returns:
Generator: Generates of reagents. Generator: Generates of reagents.
""" """
reagents = self.reagent_role.instances reagents = self.reagent_role.controls
try: try:
regex = self.uses['exclude_regex'] regex = self.uses['exclude_regex']
except KeyError: except KeyError:
@@ -1821,33 +1809,33 @@ class KitTypeReagentRoleAssociation(BaseClass):
) )
class SubmissionReagentAssociation(BaseClass): class ProcedureReagentAssociation(BaseClass):
""" """
table containing submission/reagent associations table containing run/reagent associations
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
skip_on_edit = True skip_on_edit = True
reagent_id = Column(INTEGER, ForeignKey("_reagent.id"), primary_key=True) #: id of associated reagent reagent_id = Column(INTEGER, ForeignKey("_reagent.id"), primary_key=True) #: id of associated reagent
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id"), primary_key=True) #: id of associated submission procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated run
comments = Column(String(1024)) #: Comments about reagents comments = Column(String(1024)) #: Comments about reagents
submission = relationship("BasicSubmission", procedure = relationship("Procedure",
back_populates="submission_reagent_associations") #: associated submission back_populates="procedure_reagent_associations") #: associated run
reagent = relationship(Reagent, back_populates="reagent_submission_associations") #: associated reagent reagent = relationship(Reagent, back_populates="reagent_procedure_associations") #: associated reagent
def __repr__(self) -> str: def __repr__(self) -> str:
""" """
Returns: Returns:
str: Representation of this SubmissionReagentAssociation str: Representation of this RunReagentAssociation
""" """
try: try:
return f"<SubmissionReagentAssociation({self.submission.rsl_plate_num} & {self.reagent.lot})>" return f"<RunReagentAssociation({self.procedure.run.rsl_plate_num} & {self.reagent.lot})>"
except AttributeError: except AttributeError:
logger.error(f"Reagent {self.reagent.lot} submission association {self.reagent_id} has no submissions!") logger.error(f"Reagent {self.reagent.lot} run association {self.reagent_id} has no submissions!")
return f"<SubmissionReagentAssociation(Unknown Submission & {self.reagent.lot})>" return f"<RunReagentAssociation(Unknown Submission & {self.reagent.lot})>"
def __init__(self, reagent=None, submission=None): def __init__(self, reagent=None, submission=None):
if isinstance(reagent, list): if isinstance(reagent, list):
@@ -1860,21 +1848,21 @@ class SubmissionReagentAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submission: "BasicSubmission" | str | int | None = None, run: "BasicRun" | str | int | None = None,
reagent: Reagent | str | None = None, reagent: Reagent | str | None = None,
limit: int = 0) -> SubmissionReagentAssociation | List[SubmissionReagentAssociation]: limit: int = 0) -> RunReagentAssociation | List[RunReagentAssociation]:
""" """
Lookup SubmissionReagentAssociations of interest. Lookup SubmissionReagentAssociations of interest.
Args: Args:
submission (BasicSubmission&quot; | str | int | None, optional): Identifier of joined submission. Defaults to None. run (BasicRun | str | int | None, optional): Identifier of joined run. Defaults to None.
reagent (Reagent | str | None, optional): Identifier of joined reagent. Defaults to None. reagent (Reagent | str | None, optional): Identifier of joined reagent. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns: Returns:
SubmissionReagentAssociation|List[SubmissionReagentAssociation]: SubmissionReagentAssociation(s) of interest RunReagentAssociation|List[RunReagentAssociation]: SubmissionReagentAssociation(s) of interest
""" """
from . import BasicSubmission from . import BasicRun
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match reagent: match reagent:
case Reagent() | str(): case Reagent() | str():
@@ -1883,27 +1871,27 @@ class SubmissionReagentAssociation(BaseClass):
query = query.filter(cls.reagent == reagent) query = query.filter(cls.reagent == reagent)
case _: case _:
pass pass
match submission: match run:
case BasicSubmission() | str(): case BasicRun() | str():
if isinstance(submission, str): if isinstance(run, str):
submission = BasicSubmission.query(rsl_plate_num=submission) run = BasicRun.query(rsl_plate_num=run)
query = query.filter(cls.submission == submission) query = query.filter(cls.run == run)
case int(): case int():
submission = BasicSubmission.query(id=submission) run = BasicRun.query(id=run)
query = query.join(BasicSubmission).filter(BasicSubmission.id == submission) query = query.join(BasicRun).filter(BasicRun.id == run)
case _: case _:
pass pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_sub_dict(self, extraction_kit) -> dict: def to_sub_dict(self, extraction_kit) -> dict:
""" """
Converts this SubmissionReagentAssociation (and associated Reagent) to dict Converts this RunReagentAssociation (and associated Reagent) to dict
Args: Args:
extraction_kit (_type_): Extraction kit of interest extraction_kit (_type_): Extraction kit of interest
Returns: Returns:
dict: This SubmissionReagentAssociation as dict dict: This RunReagentAssociation as dict
""" """
output = self.reagent.to_sub_dict(extraction_kit) output = self.reagent.to_sub_dict(extraction_kit)
output['comments'] = self.comments output['comments'] = self.comments
@@ -1923,20 +1911,20 @@ class Equipment(BaseClass, LogMixin):
name = Column(String(64)) #: equipment name name = Column(String(64)) #: equipment name
nickname = Column(String(64)) #: equipment nickname nickname = Column(String(64)) #: equipment nickname
asset_number = Column(String(16)) #: Given asset number (corpo nickname if you will) asset_number = Column(String(16)) #: Given asset number (corpo nickname if you will)
roles = relationship("EquipmentRole", back_populates="instances", roles = relationship("EquipmentRole", back_populates="controls",
secondary=equipmentroles_equipment) #: relation to EquipmentRoles secondary=equipmentroles_equipment) #: relation to EquipmentRoles
processes = relationship("Process", back_populates="equipment", processes = relationship("Process", back_populates="equipment",
secondary=equipment_processes) #: relation to Processes secondary=equipment_processes) #: relation to Processes
tips = relationship("Tips", back_populates="equipment", tips = relationship("Tips", back_populates="equipment",
secondary=equipment_tips) #: relation to Processes secondary=equipment_tips) #: relation to Processes
equipment_submission_associations = relationship( equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation", "RunEquipmentAssociation",
back_populates="equipment", back_populates="equipment",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) #: Association with BasicSubmission ) #: Association with BasicRun
submissions = association_proxy("equipment_submission_associations", submissions = association_proxy("equipment_submission_associations",
"submission") #: proxy to equipment_submission_associations.submission "run") #: proxy to equipment_submission_associations.run
def to_dict(self, processes: bool = False) -> dict: def to_dict(self, processes: bool = False) -> dict:
""" """
@@ -2135,7 +2123,7 @@ class EquipmentRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: Role id, primary key id = Column(INTEGER, primary_key=True) #: Role id, primary key
name = Column(String(32)) #: Common name name = Column(String(32)) #: Common name
instances = relationship("Equipment", back_populates="roles", instances = relationship("Equipment", back_populates="roles",
secondary=equipmentroles_equipment) #: Concrete instances (Equipment) of role secondary=equipmentroles_equipment) #: Concrete controls (Equipment) of role
processes = relationship("Process", back_populates='equipment_roles', processes = relationship("Process", back_populates='equipment_roles',
secondary=equipmentroles_processes) #: Associated Processes secondary=equipmentroles_processes) #: Associated Processes
@@ -2253,13 +2241,13 @@ class EquipmentRole(BaseClass):
return OmniEquipmentRole(instance_object=self, name=self.name) return OmniEquipmentRole(instance_object=self, name=self.name)
class SubmissionEquipmentAssociation(BaseClass): class RunEquipmentAssociation(BaseClass):
""" """
Abstract association between BasicSubmission and Equipment Abstract association between BasicRun and Equipment
""" """
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id"), primary_key=True) #: id of associated submission run_id = Column(INTEGER, ForeignKey("_basicrun.id"), primary_key=True) #: id of associated run
role = Column(String(64), primary_key=True) #: name of the role the equipment fills role = Column(String(64), primary_key=True) #: name of the role the equipment fills
process_id = Column(INTEGER, ForeignKey("_process.id", ondelete="SET NULL", process_id = Column(INTEGER, ForeignKey("_process.id", ondelete="SET NULL",
name="SEA_Process_id")) #: Foreign key of process id name="SEA_Process_id")) #: Foreign key of process id
@@ -2267,16 +2255,16 @@ class SubmissionEquipmentAssociation(BaseClass):
end_time = Column(TIMESTAMP) #: end time of equipment use end_time = Column(TIMESTAMP) #: end time of equipment use
comments = Column(String(1024)) #: comments about equipment comments = Column(String(1024)) #: comments about equipment
submission = relationship("BasicSubmission", run = relationship("BasicRun",
back_populates="submission_equipment_associations") #: associated submission back_populates="run_equipment_associations") #: associated run
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment equipment = relationship(Equipment, back_populates="equipment_run_associations") #: associated equipment
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<SubmissionEquipmentAssociation({self.submission.rsl_plate_num} & {self.equipment.name})>" return f"<RunEquipmentAssociation({self.run.rsl_plate_num} & {self.equipment.name})>"
def __init__(self, submission, equipment, role: str = "None"): def __init__(self, run, equipment, role: str = "None"):
self.submission = submission self.run = run
self.equipment = equipment self.equipment = equipment
self.role = role self.role = role
@@ -2286,10 +2274,10 @@ class SubmissionEquipmentAssociation(BaseClass):
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
""" """
This SubmissionEquipmentAssociation as a dictionary This RunEquipmentAssociation as a dictionary
Returns: Returns:
dict: This SubmissionEquipmentAssociation as a dictionary dict: This RunEquipmentAssociation as a dictionary
""" """
try: try:
process = self.process.name process = self.process.name
@@ -2311,12 +2299,12 @@ class SubmissionEquipmentAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, equipment_id: int | None = None, submission_id: int | None = None, role: str | None = None, def query(cls, equipment_id: int | None = None, run_id: int | None = None, role: str | None = None,
limit: int = 0, **kwargs) \ limit: int = 0, **kwargs) \
-> Any | List[Any]: -> Any | List[Any]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
query = query.filter(cls.equipment_id == equipment_id) query = query.filter(cls.equipment_id == equipment_id)
query = query.filter(cls.submission_id == submission_id) query = query.filter(cls.run_id == run_id)
if role is not None: if role is not None:
query = query.filter(cls.role == role) query = query.filter(cls.role == role)
return cls.execute_query(query=query, limit=limit, **kwargs) return cls.execute_query(query=query, limit=limit, **kwargs)
@@ -2328,13 +2316,13 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
""" """
equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"), submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
primary_key=True) #: id of associated submission primary_key=True) #: id of associated run
uses = Column(JSON) #: locations of equipment on the submission type excel sheet. uses = Column(JSON) #: locations of equipment on the run type excel sheet.
static = Column(INTEGER, static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list? default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
submission_type = relationship(SubmissionType, submission_type = relationship(SubmissionType,
back_populates="submissiontype_equipmentrole_associations") #: associated submission back_populates="submissiontype_equipmentrole_associations") #: associated run
equipment_role = relationship(EquipmentRole, equipment_role = relationship(EquipmentRole,
back_populates="equipmentrole_submissiontype_associations") #: associated equipment back_populates="equipmentrole_submissiontype_associations") #: associated equipment
@@ -2386,8 +2374,8 @@ class Process(BaseClass):
secondary=equipment_processes) #: relation to Equipment secondary=equipment_processes) #: relation to Equipment
equipment_roles = relationship("EquipmentRole", back_populates='processes', equipment_roles = relationship("EquipmentRole", back_populates='processes',
secondary=equipmentroles_processes) #: relation to EquipmentRoles secondary=equipmentroles_processes) #: relation to EquipmentRoles
submissions = relationship("SubmissionEquipmentAssociation", submissions = relationship("RunEquipmentAssociation",
backref='process') #: relation to SubmissionEquipmentAssociation backref='process') #: relation to RunEquipmentAssociation
kit_types = relationship("KitType", back_populates='processes', kit_types = relationship("KitType", back_populates='processes',
secondary=kittypes_processes) #: relation to KitType secondary=kittypes_processes) #: relation to KitType
tip_roles = relationship("TipRole", back_populates='processes', tip_roles = relationship("TipRole", back_populates='processes',
@@ -2545,14 +2533,14 @@ class TipRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type name = Column(String(64)) #: name of reagent type
instances = relationship("Tips", back_populates="role", instances = relationship("Tips", back_populates="role",
secondary=tiproles_tips) #: concrete instances of this reagent type secondary=tiproles_tips) #: concrete controls of this reagent type
processes = relationship("Process", back_populates="tip_roles", secondary=process_tiprole) processes = relationship("Process", back_populates="tip_roles", secondary=process_tiprole)
tiprole_submissiontype_associations = relationship( tiprole_submissiontype_associations = relationship(
"SubmissionTypeTipRoleAssociation", "SubmissionTypeTipRoleAssociation",
back_populates="tip_role", back_populates="tip_role",
cascade="all, delete-orphan" cascade="all, delete-orphan"
) #: associated submission ) #: associated run
submission_types = association_proxy("tiprole_submissiontype_associations", "submission_type") submission_types = association_proxy("tiprole_submissiontype_associations", "submission_type")
@@ -2608,21 +2596,21 @@ class Tips(BaseClass, LogMixin):
A concrete instance of tips. A concrete instance of tips.
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("TipRole", back_populates="instances", role = relationship("TipRole", back_populates="controls",
secondary=tiproles_tips) #: joined parent reagent type secondary=tiproles_tips) #: joined parent reagent type
role_id = Column(INTEGER, ForeignKey("_tiprole.id", ondelete='SET NULL', role_id = Column(INTEGER, ForeignKey("_tiprole.id", ondelete='SET NULL',
name="fk_tip_role_id")) #: id of parent reagent type name="fk_tip_role_id")) #: id of parent reagent type
name = Column(String(64)) #: tip common name name = Column(String(64)) #: tip common name
lot = Column(String(64)) #: lot number of tips lot = Column(String(64)) #: lot number of tips
equipment = relationship("Equipment", back_populates="tips", equipment = relationship("Equipment", back_populates="tips",
secondary=equipment_tips) #: associated submission secondary=equipment_tips) #: associated run
tips_submission_associations = relationship( tips_submission_associations = relationship(
"SubmissionTipsAssociation", "SubmissionTipsAssociation",
back_populates="tips", back_populates="tips",
cascade="all, delete-orphan" cascade="all, delete-orphan"
) #: associated submission ) #: associated run
submissions = association_proxy("tips_submission_associations", 'submission') submissions = association_proxy("tips_submission_associations", 'run')
@hybrid_property @hybrid_property
def tiprole(self): def tiprole(self):
@@ -2728,12 +2716,12 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
""" """
tiprole_id = Column(INTEGER, ForeignKey("_tiprole.id"), primary_key=True) #: id of associated equipment tiprole_id = Column(INTEGER, ForeignKey("_tiprole.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"), submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
primary_key=True) #: id of associated submission primary_key=True) #: id of associated run
uses = Column(JSON) #: locations of equipment on the submission type excel sheet. uses = Column(JSON) #: locations of equipment on the run type excel sheet.
static = Column(INTEGER, static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list? default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
submission_type = relationship(SubmissionType, submission_type = relationship(SubmissionType,
back_populates="submissiontype_tiprole_associations") #: associated submission back_populates="submissiontype_tiprole_associations") #: associated run
tip_role = relationship(TipRole, tip_role = relationship(TipRole,
back_populates="tiprole_submissiontype_associations") #: associated equipment back_populates="tiprole_submissiontype_associations") #: associated equipment
@@ -2753,16 +2741,16 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
pass pass
class SubmissionTipsAssociation(BaseClass): class RunTipsAssociation(BaseClass):
""" """
Association between a concrete submission instance and concrete tips Association between a concrete run instance and concrete tips
""" """
tip_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment tip_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id"), primary_key=True) #: id of associated submission run_id = Column(INTEGER, ForeignKey("_basicrun.id"), primary_key=True) #: id of associated run
submission = relationship("BasicSubmission", run = relationship("BasicRun",
back_populates="submission_tips_associations") #: associated submission back_populates="run_tips_associations") #: associated run
tips = relationship(Tips, tips = relationship(Tips,
back_populates="tips_submission_associations") #: associated equipment back_populates="tips_run_associations") #: associated equipment
role_name = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) role_name = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name"))
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
@@ -2776,21 +2764,21 @@ class SubmissionTipsAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) \ def query(cls, tip_id: int, role: str, run_id: int | None = None, limit: int = 0, **kwargs) \
-> Any | List[Any]: -> Any | List[Any]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
query = query.filter(cls.tip_id == tip_id) query = query.filter(cls.tip_id == tip_id)
if submission_id is not None: if run_id is not None:
query = query.filter(cls.submission_id == submission_id) query = query.filter(cls.run_id == run_id)
query = query.filter(cls.role_name == role) query = query.filter(cls.role_name == role)
return cls.execute_query(query=query, limit=limit, **kwargs) return cls.execute_query(query=query, limit=limit, **kwargs)
@classmethod @classmethod
def query_or_create(cls, tips, submission, role: str, **kwargs): def query_or_create(cls, tips, run, role: str, **kwargs):
kwargs['limit'] = 1 kwargs['limit'] = 1
instance = cls.query(tip_id=tips.id, role=role, submission_id=submission.id, **kwargs) instance = cls.query(tip_id=tips.id, role=role, run_id=run.id, **kwargs)
if instance is None: if instance is None:
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role) instance = cls(run=run, tips=tips, role_name=role)
return instance return instance
def to_pydantic(self): def to_pydantic(self):

File diff suppressed because it is too large Load Diff

View File

@@ -3,5 +3,6 @@ Contains pandas and openpyxl convenience functions for interacting with excel wo
''' '''
from .parser import * from .parser import *
from .submission_parser import *
from .reports import * from .reports import *
from .writer import * from .writer import *

View File

@@ -1,5 +1,5 @@
""" """
contains parser objects for pulling values from client generated submission sheets. contains parser objects for pulling values from client generated run sheets.
""" """
import logging import logging
from copy import copy from copy import copy
@@ -45,8 +45,8 @@ class SheetParser(object):
self.sub['submission_type'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath), self.sub['submission_type'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath),
missing=True) missing=True)
self.submission_type = SubmissionType.query(name=self.sub['submission_type']) self.submission_type = SubmissionType.query(name=self.sub['submission_type'])
self.sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
# NOTE: grab the info map from the submission type in database # NOTE: grab the info map from the run type in database
self.parse_info() self.parse_info()
self.import_kit_validation_check() self.import_kit_validation_check()
self.parse_reagents() self.parse_reagents()
@@ -60,17 +60,17 @@ class SheetParser(object):
""" """
parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object) parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object)
self.info_map = parser.info_map self.info_map = parser.info_map
# NOTE: in order to accommodate generic submission types we have to check for the type in the excel sheet and rerun accordingly # NOTE: in order to accommodate generic run types we have to check for the type in the excel sheet and rerun accordingly
try: try:
check = parser.parsed_info['submission_type']['value'] not in [None, "None", "", " "] check = parser.parsed_info['submission_type']['value'] not in [None, "None", "", " "]
except KeyError as e: except KeyError as e:
logger.error(f"Couldn't check submission type due to KeyError: {e}") logger.error(f"Couldn't check run type due to KeyError: {e}")
return return
logger.info( logger.info(
f"Checking for updated submission type: {self.submission_type.name} against new: {parser.parsed_info['submission_type']['value']}") f"Checking for updated run type: {self.submission_type.name} against new: {parser.parsed_info['submission_type']['value']}")
if self.submission_type.name != parser.parsed_info['submission_type']['value']: if self.submission_type.name != parser.parsed_info['submission_type']['value']:
if check: if check:
# NOTE: If initial submission type doesn't match parsed submission type, defer to parsed submission type. # NOTE: If initial run type doesn't match parsed run type, defer to parsed run type.
self.submission_type = SubmissionType.query(name=parser.parsed_info['submission_type']['value']) self.submission_type = SubmissionType.query(name=parser.parsed_info['submission_type']['value'])
logger.info(f"Updated self.submission_type to {self.submission_type}. Rerunning parse.") logger.info(f"Updated self.submission_type to {self.submission_type}. Rerunning parse.")
self.parse_info() self.parse_info()
@@ -145,17 +145,17 @@ class InfoParser(object):
Object to parse generic info from excel sheet. Object to parse generic info from excel sheet.
""" """
def __init__(self, xl: Workbook, submission_type: str | SubmissionType, sub_object: BasicSubmission | None = None): def __init__(self, xl: Workbook, submission_type: str | SubmissionType, sub_object: BasicRun | None = None):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None. sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
if sub_object is None: if sub_object is None:
sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name) sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=submission_type.name)
self.submission_type_obj = submission_type self.submission_type_obj = submission_type
self.submission_type = dict(value=self.submission_type_obj.name, missing=True) self.submission_type = dict(value=self.submission_type_obj.name, missing=True)
self.sub_object = sub_object self.sub_object = sub_object
@@ -167,9 +167,9 @@ class InfoParser(object):
Gets location of basic info from the submission_type object in the database. Gets location of basic info from the submission_type object in the database.
Returns: Returns:
dict: Location map of all info for this submission type dict: Location map of all info for this run type
""" """
# NOTE: Get the parse_info method from the submission type specified # NOTE: Get the parse_info method from the run type specified
return self.sub_object.construct_info_map(submission_type=self.submission_type_obj, mode="read") return self.sub_object.construct_info_map(submission_type=self.submission_type_obj, mode="read")
@property @property
@@ -232,7 +232,7 @@ class InfoParser(object):
dicto[item['name']] = dict(value=value, missing=missing) dicto[item['name']] = dict(value=value, missing=missing)
except (KeyError, IndexError): except (KeyError, IndexError):
continue continue
# NOTE: Return after running the parser components held in submission object. # NOTE: Return after running the parser components held in run object.
return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl, custom_fields=self.info_map['custom']) return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl, custom_fields=self.info_map['custom'])
@@ -242,20 +242,20 @@ class ReagentParser(object):
""" """
def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str, def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str,
sub_object: BasicSubmission | None = None): run_object: BasicRun | None = None):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str|SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (str|SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
extraction_kit (str): Extraction kit used. extraction_kit (str): Extraction kit used.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None. run_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type_obj = submission_type self.submission_type_obj = submission_type
if not sub_object: if not run_object:
sub_object = submission_type.submission_class run_object = submission_type.submission_class
self.sub_object = sub_object self.run_object = run_object
if isinstance(extraction_kit, dict): if isinstance(extraction_kit, dict):
extraction_kit = extraction_kit['value'] extraction_kit = extraction_kit['value']
self.kit_object = KitType.query(name=extraction_kit) self.kit_object = KitType.query(name=extraction_kit)
@@ -267,7 +267,7 @@ class ReagentParser(object):
Gets location of kit reagents from database Gets location of kit reagents from database
Args: Args:
submission_type (str): Name of submission type. submission_type (str): Name of run type.
Returns: Returns:
dict: locations of reagent info for the kit. dict: locations of reagent info for the kit.
@@ -327,13 +327,13 @@ class SampleParser(object):
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None,
sub_object: BasicSubmission | None = None) -> None: sub_object: BasicRun | None = None) -> None:
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None. sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None. sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
""" """
self.samples = [] self.samples = []
self.xl = xl self.xl = xl
@@ -343,8 +343,8 @@ class SampleParser(object):
self.submission_type_obj = submission_type self.submission_type_obj = submission_type
if sub_object is None: if sub_object is None:
logger.warning( logger.warning(
f"Sample parser attempting to fetch submission class with polymorphic identity: {self.submission_type}") f"Sample parser attempting to fetch run class with polymorphic identity: {self.submission_type}")
sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.sub_object = sub_object self.sub_object = sub_object
self.sample_type = self.sub_object.get_default_info("sample_type", submission_type=submission_type) self.sample_type = self.sub_object.get_default_info("sample_type", submission_type=submission_type)
self.samp_object = BasicSample.find_polymorphic_subclass(polymorphic_identity=self.sample_type) self.samp_object = BasicSample.find_polymorphic_subclass(polymorphic_identity=self.sample_type)
@@ -352,10 +352,10 @@ class SampleParser(object):
@property @property
def sample_map(self) -> dict: def sample_map(self) -> dict:
""" """
Gets info locations in excel book for submission type. Gets info locations in excel book for run type.
Args: Args:
submission_type (str): submission type submission_type (str): run type
Returns: Returns:
dict: Info locations. dict: Info locations.
@@ -478,7 +478,7 @@ class EquipmentParser(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -488,7 +488,7 @@ class EquipmentParser(object):
@property @property
def equipment_map(self) -> dict: def equipment_map(self) -> dict:
""" """
Gets the map of equipment locations in the submission type's spreadsheet Gets the map of equipment locations in the run type's spreadsheet
Returns: Returns:
List[dict]: List of locations List[dict]: List of locations
@@ -556,7 +556,7 @@ class TipParser(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -566,7 +566,7 @@ class TipParser(object):
@property @property
def tip_map(self) -> dict: def tip_map(self) -> dict:
""" """
Gets the map of equipment locations in the submission type's spreadsheet Gets the map of equipment locations in the run type's spreadsheet
Returns: Returns:
List[dict]: List of locations List[dict]: List of locations
@@ -609,11 +609,11 @@ class TipParser(object):
class PCRParser(object): class PCRParser(object):
"""Object to pull data from Design and Analysis PCR export file.""" """Object to pull data from Design and Analysis PCR export file."""
def __init__(self, filepath: Path | None = None, submission: BasicSubmission | None = None) -> None: def __init__(self, filepath: Path | None = None, submission: BasicRun | None = None) -> None:
""" """
Args: Args:
filepath (Path | None, optional): file to parse. Defaults to None. filepath (Path | None, optional): file to parse. Defaults to None.
submission (BasicSubmission | None, optional): Submission parsed data to be added to. submission (BasicRun | None, optional): Submission parsed data to be added to.
""" """
if filepath is None: if filepath is None:
logger.error('No filepath given.') logger.error('No filepath given.')
@@ -659,7 +659,7 @@ class PCRParser(object):
class ConcentrationParser(object): class ConcentrationParser(object):
def __init__(self, filepath: Path | None = None, submission: BasicSubmission | None = None) -> None: def __init__(self, filepath: Path | None = None, run: BasicRun | None = None) -> None:
if filepath is None: if filepath is None:
logger.error('No filepath given.') logger.error('No filepath given.')
self.xl = None self.xl = None
@@ -672,11 +672,11 @@ class ConcentrationParser(object):
except PermissionError: except PermissionError:
logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.") logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.")
return None return None
if submission is None: if run is None:
self.submission_obj = BacterialCulture self.submission_obj = BasicRun()
rsl_plate_num = None rsl_plate_num = None
else: else:
self.submission_obj = submission self.submission_obj = run
rsl_plate_num = self.submission_obj.rsl_plate_num rsl_plate_num = self.submission_obj.rsl_plate_num
self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_num=rsl_plate_num) self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_num=rsl_plate_num)

View File

@@ -7,7 +7,7 @@ from pandas import DataFrame, ExcelWriter
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
from typing import Tuple, List from typing import Tuple, List
from backend.db.models import BasicSubmission from backend.db.models import BasicRun
from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
@@ -45,9 +45,9 @@ class ReportMaker(object):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# 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.runs = BasicRun.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.client_submission.submitting_lab.name in organizations] self.runs = [run for run in self.runs if run.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)
@@ -58,9 +58,9 @@ class ReportMaker(object):
Returns: Returns:
DataFrame: output dataframe DataFrame: output dataframe
""" """
if not self.subs: if not self.runs:
return DataFrame(), DataFrame() return DataFrame(), DataFrame()
df = DataFrame.from_records([item.to_dict(report=True) for item in self.subs]) df = DataFrame.from_records([item.to_dict(report=True) for item in self.runs])
# NOTE: put submissions with the same lab together # NOTE: put submissions with the same lab together
df = df.sort_values("submitting_lab") df = df.sort_values("submitting_lab")
# NOTE: aggregate cost and sample count columns # NOTE: aggregate cost and sample count columns
@@ -156,19 +156,19 @@ class TurnaroundMaker(ReportArchetype):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# 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, self.subs = BasicRun.query(start_date=start_date, end_date=end_date,
submission_type_name=submission_type, page_size=0) submission_type_name=submission_type, page_size=0)
records = [self.build_record(sub) for sub in self.subs] records = [self.build_record(sub) for sub in self.subs]
self.df = DataFrame.from_records(records) self.df = DataFrame.from_records(records)
self.sheet_name = "Turnaround" self.sheet_name = "Turnaround"
@classmethod @classmethod
def build_record(cls, sub: BasicSubmission) -> dict: def build_record(cls, sub: BasicRun) -> dict:
""" """
Build a turnaround dictionary from a submission Build a turnaround dictionary from a run
Args: Args:
sub (BasicSubmission): The submission to be processed. sub (BasicRun): The run to be processed.
Returns: Returns:
@@ -203,9 +203,9 @@ class ConcentrationMaker(ReportArchetype):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# 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, self.subs = BasicRun.query(start_date=start_date, end_date=end_date,
submission_type_name=submission_type, page_size=0) submission_type_name=submission_type, page_size=0)
# self.samples = flatten_list([sub.get_provisional_controls(controls_only=controls_only) for sub in self.subs]) # self.samples = flatten_list([sub.get_provisional_controls(controls_only=controls_only) for sub in self.runs])
self.samples = flatten_list([sub.get_provisional_controls(include=include) for sub in self.subs]) self.samples = flatten_list([sub.get_provisional_controls(include=include) for sub in self.subs])
self.records = [self.build_record(sample) for sample in self.samples] self.records = [self.build_record(sample) for sample in self.samples]
self.df = DataFrame.from_records(self.records) self.df = DataFrame.from_records(self.records)

View File

@@ -1,5 +1,5 @@
""" """
contains writer objects for pushing values to submission sheet templates. contains writer objects for pushing values to run sheet templates.
""" """
import logging import logging
from copy import copy from copy import copy
@@ -8,7 +8,7 @@ from operator import itemgetter
from pprint import pformat from pprint import pformat
from typing import List, Generator, Tuple from typing import List, Generator, Tuple
from openpyxl import load_workbook, Workbook from openpyxl import load_workbook, Workbook
from backend.db.models import SubmissionType, KitType, BasicSubmission from backend.db.models import SubmissionType, KitType, BasicRun
from backend.validators.pydant import PydSubmission from backend.validators.pydant import PydSubmission
from io import BytesIO from io import BytesIO
from collections import OrderedDict from collections import OrderedDict
@@ -24,7 +24,7 @@ class SheetWriter(object):
def __init__(self, submission: PydSubmission): def __init__(self, submission: PydSubmission):
""" """
Args: Args:
submission (PydSubmission): Object containing submission information. submission (PydSubmission): Object containing run information.
""" """
self.sub = OrderedDict(submission.improved_dict()) self.sub = OrderedDict(submission.improved_dict())
# NOTE: Set values from pydantic object. # NOTE: Set values from pydantic object.
@@ -35,7 +35,7 @@ class SheetWriter(object):
case 'submission_type': case 'submission_type':
self.sub[k] = v['value'] self.sub[k] = v['value']
self.submission_type = SubmissionType.query(name=v['value']) self.submission_type = SubmissionType.query(name=v['value'])
self.sub_object = BasicSubmission.find_polymorphic_subclass( self.run_object = BasicRun.find_polymorphic_subclass(
polymorphic_identity=self.submission_type) polymorphic_identity=self.submission_type)
case _: case _:
if isinstance(v, dict): if isinstance(v, dict):
@@ -99,22 +99,22 @@ class SheetWriter(object):
class InfoWriter(object): class InfoWriter(object):
""" """
object to write general submission info into excel file object to write general run info into excel file
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict,
sub_object: BasicSubmission | None = None): sub_object: BasicRun | None = None):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.)
info_dict (dict): Dictionary of information to write. info_dict (dict): Dictionary of information to write.
sub_object (BasicSubmission | None, optional): Submission object containing methods. Defaults to None. sub_object (BasicRun | None, optional): Submission object containing methods. Defaults to None.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
if sub_object is None: if sub_object is None:
sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=submission_type.name) sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=submission_type.name)
self.submission_type = submission_type self.submission_type = submission_type
self.sub_object = sub_object self.sub_object = sub_object
self.xl = xl self.xl = xl
@@ -196,7 +196,7 @@ class ReagentWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.)
extraction_kit (KitType | str): Extraction kit used. extraction_kit (KitType | str): Extraction kit used.
reagent_list (list): List of reagent dicts to be written to excel. reagent_list (list): List of reagent dicts to be written to excel.
""" """
@@ -273,7 +273,7 @@ class SampleWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.)
sample_list (list): List of sample dictionaries to be written to excel file. sample_list (list): List of sample dictionaries to be written to excel file.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
@@ -281,7 +281,7 @@ class SampleWriter(object):
self.submission_type = submission_type self.submission_type = submission_type
self.xl = xl self.xl = xl
self.sample_map = submission_type.sample_map['lookup_table'] self.sample_map = submission_type.sample_map['lookup_table']
# NOTE: exclude any samples without a submission rank. # NOTE: exclude any samples without a run rank.
samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0] samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0]
self.samples = sorted(samples, key=itemgetter('submission_rank')) self.samples = sorted(samples, key=itemgetter('submission_rank'))
self.blank_lookup_table() self.blank_lookup_table()
@@ -351,7 +351,7 @@ class EquipmentWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.)
equipment_list (list): List of equipment dictionaries to write to excel file. equipment_list (list): List of equipment dictionaries to write to excel file.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):
@@ -433,7 +433,7 @@ class TipWriter(object):
""" """
Args: Args:
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (SubmissionType | str): Type of run expected (Wastewater, Bacterial Culture, etc.)
tips_list (list): List of tip dictionaries to write to the excel file. tips_list (list): List of tip dictionaries to write to the excel file.
""" """
if isinstance(submission_type, str): if isinstance(submission_type, str):

View File

@@ -5,7 +5,7 @@ import logging, re
import sys import sys
from pathlib import Path from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
from backend.db.models import BasicSubmission, SubmissionType from backend.db.models import BasicRun, SubmissionType
from tools import jinja_template_loading from tools import jinja_template_loading
from jinja2 import Template from jinja2 import Template
from dateutil.parser import parse from dateutil.parser import parse
@@ -25,9 +25,9 @@ class RSLNamer(object):
self.submission_type = submission_type self.submission_type = submission_type
if not self.submission_type: if not self.submission_type:
self.submission_type = self.retrieve_submission_type(filename=filename) self.submission_type = self.retrieve_submission_type(filename=filename)
logger.info(f"got submission type: {self.submission_type}") logger.info(f"got run type: {self.submission_type}")
if self.submission_type: if self.submission_type:
self.sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex( self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex(
submission_type=submission_type)) submission_type=submission_type))
if not data: if not data:
@@ -40,7 +40,7 @@ class RSLNamer(object):
@classmethod @classmethod
def retrieve_submission_type(cls, filename: str | Path) -> str: def retrieve_submission_type(cls, filename: str | Path) -> str:
""" """
Gets submission type from excel file properties or sheet names or regex pattern match or user input Gets run type from excel file properties or sheet names or regex pattern match or user input
Args: Args:
filename (str | Path): filename filename (str | Path): filename
@@ -49,12 +49,12 @@ class RSLNamer(object):
TypeError: Raised if unsupported variable type for filename given. TypeError: Raised if unsupported variable type for filename given.
Returns: Returns:
str: parsed submission type str: parsed run type
""" """
def st_from_path(filepath: Path) -> str: def st_from_path(filepath: Path) -> str:
""" """
Sub def to get submissiontype from a file path Sub def to get proceduretype from a file path
Args: Args:
filepath (): filepath ():
@@ -83,13 +83,13 @@ class RSLNamer(object):
def st_from_str(file_name: str) -> str: def st_from_str(file_name: str) -> str:
if file_name.startswith("tmp"): if file_name.startswith("tmp"):
return "Bacterial Culture" return "Bacterial Culture"
regex = BasicSubmission.regex regex = BasicRun.regex
m = regex.search(file_name) m = regex.search(file_name)
try: try:
sub_type = m.lastgroup sub_type = m.lastgroup
except AttributeError as e: except AttributeError as e:
sub_type = None sub_type = None
logger.critical(f"No submission type found or submission type found!: {e}") logger.critical(f"No run type found or run type found!: {e}")
return sub_type return sub_type
match filename: match filename:
@@ -107,8 +107,8 @@ class RSLNamer(object):
if "pytest" in sys.modules: if "pytest" in sys.modules:
raise ValueError("Submission Type came back as None.") raise ValueError("Submission Type came back as None.")
from frontend.widgets import ObjectSelector from frontend.widgets import ObjectSelector
dlg = ObjectSelector(title="Couldn't parse submission type.", dlg = ObjectSelector(title="Couldn't parse run type.",
message="Please select submission type from list below.", message="Please select run type from list below.",
obj_type=SubmissionType) obj_type=SubmissionType)
if dlg.exec(): if dlg.exec():
submission_type = dlg.parse_form() submission_type = dlg.parse_form()
@@ -118,14 +118,14 @@ class RSLNamer(object):
@classmethod @classmethod
def retrieve_rsl_number(cls, filename: str | Path, regex: re.Pattern | None = None): def retrieve_rsl_number(cls, filename: str | Path, regex: re.Pattern | None = None):
""" """
Uses regex to retrieve the plate number and submission type from an input string Uses regex to retrieve the plate number and run type from an input string
Args: Args:
regex (str): string to construct pattern regex (str): string to construct pattern
filename (str): string to be parsed filename (str): string to be parsed
""" """
if regex is None: if regex is None:
regex = BasicSubmission.regex regex = BasicRun.regex
match filename: match filename:
case Path(): case Path():
m = regex.search(filename.stem) m = regex.search(filename.stem)
@@ -145,10 +145,10 @@ class RSLNamer(object):
@classmethod @classmethod
def construct_new_plate_name(cls, data: dict) -> str: def construct_new_plate_name(cls, data: dict) -> str:
""" """
Make a brand-new plate name from submission data. Make a brand-new plate name from run data.
Args: Args:
data (dict): incoming submission data data (dict): incoming run data
Returns: Returns:
str: Output filename str: Output filename
@@ -170,7 +170,7 @@ class RSLNamer(object):
if "rsl_plate_num" in data.keys(): if "rsl_plate_num" in data.keys():
plate_number = data['rsl_plate_num'].split("-")[-1][0] plate_number = data['rsl_plate_num'].split("-")[-1][0]
else: else:
previous = BasicSubmission.query(start_date=today, end_date=today, submissiontype=data['submission_type']) previous = BasicRun.query(start_date=today, end_date=today, submissiontype=data['submission_type'])
plate_number = len(previous) + 1 plate_number = len(previous) + 1
return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}" return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
@@ -180,7 +180,7 @@ class RSLNamer(object):
Make export file name from jinja template. (currently unused) Make export file name from jinja template. (currently unused)
Args: Args:
template (jinja2.Template): Template stored in BasicSubmission template (jinja2.Template): Template stored in BasicRun
Returns: Returns:
str: output file name. str: output file name.

View File

@@ -270,7 +270,7 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni):
except AttributeError: except AttributeError:
return f"<{self.__class__.__name__}(NO NAME)>" return f"<{self.__class__.__name__}(NO NAME)>"
@field_validator("submissiontype", mode="before") @field_validator("proceduretype", mode="before")
@classmethod @classmethod
def rescue_submissiontype_none(cls, value): def rescue_submissiontype_none(cls, value):
if not value: if not value:
@@ -324,7 +324,7 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni):
Convert this object to an instance of its class object. Convert this object to an instance of its class object.
""" """
# logger.debug(f"Self kittype: {self.submissiontype}") # logger.debug(f"Self kittype: {self.proceduretype}")
if issubclass(self.submissiontype.__class__, BaseOmni): if issubclass(self.submissiontype.__class__, BaseOmni):
submissiontype = SubmissionType.query(name=self.submissiontype.name) submissiontype = SubmissionType.query(name=self.submissiontype.name)
else: else:
@@ -334,7 +334,7 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni):
else: else:
kittype = KitType.query(name=self.kittype) kittype = KitType.query(name=self.kittype)
# logger.debug(f"Self kittype: {self.kittype}") # logger.debug(f"Self kittype: {self.kittype}")
# logger.debug(f"Query or create with {kittype}, {submissiontype}") # logger.debug(f"Query or create with {kittype}, {proceduretype}")
instance, is_new = self.class_object.query_or_create(kittype=kittype, submissiontype=submissiontype) instance, is_new = self.class_object.query_or_create(kittype=kittype, submissiontype=submissiontype)
instance.mutable_cost_column = self.mutable_cost_column instance.mutable_cost_column = self.mutable_cost_column
instance.mutable_cost_sample = self.mutable_cost_sample instance.mutable_cost_sample = self.mutable_cost_sample

View File

@@ -121,7 +121,7 @@ class PydReagent(BaseModel):
return {k: getattr(self, k) for k in fields} return {k: getattr(self, k) for k in fields}
@report_result @report_result
def to_sql(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, Report]: def to_sql(self, submission: BasicRun | str = None) -> Tuple[Reagent, Report]:
""" """
Converts this instance into a backend.db.models.kit.Reagent instance Converts this instance into a backend.db.models.kit.Reagent instance
@@ -141,7 +141,7 @@ class PydReagent(BaseModel):
# NOTE: reagent method sets fields based on keys in dictionary # NOTE: reagent method sets fields based on keys in dictionary
reagent.set_attribute(key, value) reagent.set_attribute(key, value)
if submission is not None and reagent not in submission.reagents: if submission is not None and reagent not in submission.reagents:
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission) assoc = RunReagentAssociation(reagent=reagent, submission=submission)
assoc.comments = self.comment assoc.comments = self.comment
else: else:
assoc = None assoc = None
@@ -198,13 +198,13 @@ class PydSample(BaseModel, extra='allow'):
return value return value
@report_result @report_result
def to_sql(self, submission: BasicSubmission | str = None) -> Tuple[ def to_sql(self, run: BasicRun | str = None) -> Tuple[
BasicSample, List[SubmissionSampleAssociation], Result | None]: BasicSample, List[SubmissionSampleAssociation], Result | None]:
""" """
Converts this instance into a backend.db.models.submissions.Sample object Converts this instance into a backend.db.models.submissions.Sample object
Args: Args:
submission (BasicSubmission | str, optional): Submission joined to this sample. Defaults to None. run (BasicRun | str, optional): Submission joined to this sample. Defaults to None.
Returns: Returns:
Tuple[BasicSample, Result]: Sample object and result object. Tuple[BasicSample, Result]: Sample object and result object.
@@ -220,13 +220,13 @@ class PydSample(BaseModel, extra='allow'):
case _: case _:
instance.__setattr__(key, value) instance.__setattr__(key, value)
out_associations = [] out_associations = []
if submission is not None: if run is not None:
if isinstance(submission, str): if isinstance(run, str):
submission = BasicSubmission.query(rsl_plate_num=submission) run = BasicRun.query(rsl_plate_num=run)
assoc_type = submission.submission_type_name assoc_type = run.submission_type_name
for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank): for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank):
association = SubmissionSampleAssociation.query_or_create(association_type=f"{assoc_type} Association", association = SubmissionSampleAssociation.query_or_create(association_type=f"{assoc_type} Association",
submission=submission, submission=run,
sample=instance, sample=instance,
row=row, column=column, id=aid, row=row, column=column, id=aid,
submission_rank=submission_rank, submission_rank=submission_rank,
@@ -234,7 +234,7 @@ class PydSample(BaseModel, extra='allow'):
try: try:
out_associations.append(association) out_associations.append(association)
except IntegrityError as e: except IntegrityError as e:
logger.error(f"Could not attach submission sample association due to: {e}") logger.error(f"Could not attach run sample association due to: {e}")
instance.metadata.session.rollback() instance.metadata.session.rollback()
return instance, out_associations, report return instance, out_associations, report
@@ -267,20 +267,20 @@ class PydTips(BaseModel):
return value return value
@report_result @report_result
def to_sql(self, submission: BasicSubmission) -> SubmissionTipsAssociation: def to_sql(self, submission: BasicRun) -> SubmissionTipsAssociation:
""" """
Convert this object to the SQL version for database storage. Convert this object to the SQL version for database storage.
Args: Args:
submission (BasicSubmission): A submission object to associate tips represented here. submission (BasicRun): A run object to associate tips represented here.
Returns: Returns:
SubmissionTipsAssociation: Association between queried tips and submission SubmissionTipsAssociation: Association between queried tips and run
""" """
report = Report() report = Report()
tips = Tips.query(name=self.name, limit=1) tips = Tips.query(name=self.name, limit=1)
# logger.debug(f"Tips query has yielded: {tips}") # logger.debug(f"Tips query has yielded: {tips}")
assoc = SubmissionTipsAssociation.query_or_create(tips=tips, submission=submission, role=self.role, limit=1) assoc = SubmissionTipsAssociation.query_or_create(tips=tips, run=submission, role=self.role, limit=1)
return assoc, report return assoc, report
@@ -315,19 +315,19 @@ class PydEquipment(BaseModel, extra='ignore'):
return value return value
@report_result @report_result
def to_sql(self, submission: BasicSubmission | str = None, extraction_kit: KitType | str = None) -> Tuple[Equipment, SubmissionEquipmentAssociation]: def to_sql(self, submission: BasicRun | str = None, extraction_kit: KitType | str = None) -> Tuple[Equipment, RunEquipmentAssociation]:
""" """
Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment
Args: Args:
submission ( BasicSubmission | str ): BasicSubmission of interest submission ( BasicRun | str ): BasicRun of interest
Returns: Returns:
Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects Tuple[Equipment, RunEquipmentAssociation]: SQL objects
""" """
report = Report() report = Report()
if isinstance(submission, str): if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission) submission = BasicRun.query(rsl_plate_num=submission)
if isinstance(extraction_kit, str): if isinstance(extraction_kit, str):
extraction_kit = KitType.query(name=extraction_kit) extraction_kit = KitType.query(name=extraction_kit)
equipment = Equipment.query(asset_number=self.asset_number) equipment = Equipment.query(asset_number=self.asset_number)
@@ -335,15 +335,15 @@ class PydEquipment(BaseModel, extra='ignore'):
logger.error("No equipment found. Returning None.") logger.error("No equipment found. Returning None.")
return return
if submission is not None: if submission is not None:
# NOTE: Need to make sure the same association is not added to the submission # NOTE: Need to make sure the same association is not added to the run
try: try:
assoc = SubmissionEquipmentAssociation.query(equipment_id=equipment.id, submission_id=submission.id, assoc = RunEquipmentAssociation.query(equipment_id=equipment.id, submission_id=submission.id,
role=self.role, limit=1) role=self.role, limit=1)
except TypeError as e: except TypeError as e:
logger.error(f"Couldn't get association due to {e}, returning...") logger.error(f"Couldn't get association due to {e}, returning...")
assoc = None assoc = None
if assoc is None: if assoc is None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment) assoc = RunEquipmentAssociation(submission=submission, equipment=equipment)
# TODO: This seems precarious. What if there is more than one process? # TODO: This seems precarious. What if there is more than one process?
# NOTE: It looks like the way fetching the processes is done in the SQL model, this shouldn't be a problem, but I'll include a failsafe. # NOTE: It looks like the way fetching the processes is done in the SQL model, this shouldn't be a problem, but I'll include a failsafe.
# NOTE: I need to find a way to filter this by the kit involved. # NOTE: I need to find a way to filter this by the kit involved.
@@ -360,7 +360,7 @@ class PydEquipment(BaseModel, extra='ignore'):
logger.warning(f"Found already existing association: {assoc}") logger.warning(f"Found already existing association: {assoc}")
assoc = None assoc = None
else: else:
logger.warning(f"No submission found") logger.warning(f"No run found")
assoc = None assoc = None
return equipment, assoc, report return equipment, assoc, report
@@ -519,7 +519,7 @@ class PydSubmission(BaseModel, extra='allow'):
value['value'] = value['value'].strip() value['value'] = value['value'].strip()
return value return value
else: else:
if "pytest" in sys.modules and sub_type.replace(" ", "") == "BasicSubmission": if "pytest" in sys.modules and sub_type.replace(" ", "") == "BasicRun":
output = "RSL-BS-Test001" output = "RSL-BS-Test001"
else: else:
output = RSLNamer(filename=values.data['filepath'].__str__(), submission_type=sub_type, output = RSLNamer(filename=values.data['filepath'].__str__(), submission_type=sub_type,
@@ -674,7 +674,7 @@ class PydSubmission(BaseModel, extra='allow'):
def __init__(self, run_custom: bool = False, **data): def __init__(self, run_custom: bool = False, **data):
super().__init__(**data) super().__init__(**data)
# NOTE: this could also be done with default_factory # NOTE: this could also be done with default_factory
self.submission_object = BasicSubmission.find_polymorphic_subclass( self.submission_object = BasicRun.find_polymorphic_subclass(
polymorphic_identity=self.submission_type['value']) polymorphic_identity=self.submission_type['value'])
self.namer = RSLNamer(self.rsl_plate_num['value'], submission_type=self.submission_type['value']) self.namer = RSLNamer(self.rsl_plate_num['value'], submission_type=self.submission_type['value'])
if run_custom: if run_custom:
@@ -771,18 +771,18 @@ class PydSubmission(BaseModel, extra='allow'):
return missing_info, missing_reagents return missing_info, missing_reagents
@report_result @report_result
def to_sql(self) -> Tuple[BasicSubmission | None, Report]: def to_sql(self) -> Tuple[BasicRun | None, Report]:
""" """
Converts this instance into a backend.db.models.submissions.BasicSubmission instance Converts this instance into a backend.db.models.submissions.BasicRun instance
Returns: Returns:
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object Tuple[BasicRun, Result]: BasicRun instance, result object
""" """
report = Report() report = Report()
dicto = self.improved_dict() dicto = self.improved_dict()
# logger.debug(f"Pydantic submission type: {self.submission_type['value']}") # logger.debug(f"Pydantic run type: {self.submission_type['value']}")
# logger.debug(f"Pydantic improved_dict: {pformat(dicto)}") # logger.debug(f"Pydantic improved_dict: {pformat(dicto)}")
instance, result = BasicSubmission.query_or_create(submission_type=self.submission_type['value'], instance, result = BasicRun.query_or_create(submission_type=self.submission_type['value'],
rsl_plate_num=self.rsl_plate_num['value']) rsl_plate_num=self.rsl_plate_num['value'])
# logger.debug(f"Created or queried instance: {instance}") # logger.debug(f"Created or queried instance: {instance}")
if instance is None: if instance is None:
@@ -808,7 +808,7 @@ class PydSubmission(BaseModel, extra='allow'):
reagent = reagent.to_sql(submission=instance) reagent = reagent.to_sql(submission=instance)
case "samples": case "samples":
for sample in self.samples: for sample in self.samples:
sample, associations, _ = sample.to_sql(submission=instance) sample, associations, _ = sample.to_sql(run=instance)
for assoc in associations: for assoc in associations:
if assoc is not None: if assoc is not None:
if assoc not in instance.submission_sample_associations: if assoc not in instance.submission_sample_associations:
@@ -1186,7 +1186,7 @@ class PydEquipmentRole(BaseModel):
Args: Args:
parent (_type_): parent widget parent (_type_): parent widget
used (list): list of equipment already added to submission used (list): list of equipment already added to run
Returns: Returns:
RoleComboBox: widget RoleComboBox: widget
@@ -1203,7 +1203,7 @@ class PydPCRControl(BaseModel):
ct: float ct: float
reagent_lot: str reagent_lot: str
submitted_date: datetime #: Date submitted to Robotics submitted_date: datetime #: Date submitted to Robotics
submission_id: int run_id: int
controltype_name: str controltype_name: str
@report_result @report_result
@@ -1231,7 +1231,7 @@ class PydIridaControl(BaseModel, extra='ignore'):
kraken2_db_version: str kraken2_db_version: str
sample_id: int sample_id: int
submitted_date: datetime #: Date submitted to Robotics submitted_date: datetime #: Date submitted to Robotics
submission_id: int run_id: int
controltype_name: str controltype_name: str
@field_validator("refseq_version", "kraken2_version", "kraken2_db_version", mode='before') @field_validator("refseq_version", "kraken2_version", "kraken2_db_version", mode='before')

View File

@@ -28,12 +28,12 @@ class ConcentrationsChart(CustomFigure):
self.df = df self.df = df
try: try:
self.df = self.df[self.df.concentration.notnull()] self.df = self.df[self.df.concentration.notnull()]
self.df = self.df.sort_values(['submitted_date', 'submission'], ascending=[True, True]).reset_index( self.df = self.df.sort_values(['submitted_date', 'run'], ascending=[True, True]).reset_index(
drop=True) drop=True)
self.df = self.df.reset_index().rename(columns={"index": "idx"}) self.df = self.df.reset_index().rename(columns={"index": "idx"})
# logger.debug(f"DF after changes:\n{self.df}") # logger.debug(f"DF after changes:\n{self.df}")
scatter = px.scatter(data_frame=self.df, x='submission', y="concentration", scatter = px.scatter(data_frame=self.df, x='run', y="concentration",
hover_data=["name", "submission", "submitted_date", "concentration"], hover_data=["name", "run", "submitted_date", "concentration"],
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"} color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}
) )
except (ValueError, AttributeError) as e: except (ValueError, AttributeError) as e:
@@ -44,11 +44,11 @@ class ConcentrationsChart(CustomFigure):
for trace in traces: for trace in traces:
self.add_trace(trace) self.add_trace(trace)
try: try:
tickvals = self.df['submission'].tolist() tickvals = self.df['run'].tolist()
except KeyError: except KeyError:
tickvals = [] tickvals = []
try: try:
ticklabels = self.df['submission'].tolist() ticklabels = self.df['run'].tolist()
except KeyError: except KeyError:
ticklabels = [] ticklabels = []
self.update_layout( self.update_layout(

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from pandas import ExcelWriter from pandas import ExcelWriter
from backend import Reagent, BasicSample, Organization, KitType, BasicSubmission from backend import Reagent, BasicSample, Organization, KitType, BasicRun
from tools import ( from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development under_development
@@ -211,7 +211,7 @@ class App(QMainWindow):
dlg = DateTypePicker(self) dlg = DateTypePicker(self)
if dlg.exec(): if dlg.exec():
output = dlg.parse_form() output = dlg.parse_form()
df = BasicSubmission.archive_submissions(**output) df = BasicRun.archive_submissions(**output)
filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx") filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx")
writer = ExcelWriter(filepath, "openpyxl") writer = ExcelWriter(filepath, "openpyxl")
df.to_excel(writer) df.to_excel(writer)
@@ -239,7 +239,7 @@ class AddSubForm(QWidget):
self.tabs.addTab(self.tab3, "PCR Controls") self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report") self.tabs.addTab(self.tab4, "Cost Report")
self.tabs.addTab(self.tab5, "Turnaround Times") self.tabs.addTab(self.tab5, "Turnaround Times")
# NOTE: Create submission adder form # NOTE: Create run adder form
self.formwidget = SubmissionFormContainer(self) self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self) self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout) self.formwidget.setLayout(self.formlayout)

View File

@@ -6,7 +6,7 @@ from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
) )
from backend.db.models import Equipment, BasicSubmission, Process from backend.db.models import Equipment, BasicRun, Process
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging import logging
from typing import Generator from typing import Generator
@@ -16,7 +16,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog): class EquipmentUsage(QDialog):
def __init__(self, parent, submission: BasicSubmission): def __init__(self, parent, submission: BasicRun):
super().__init__(parent) super().__init__(parent)
self.submission = submission self.submission = submission
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}") self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
@@ -137,7 +137,7 @@ class RoleComboBox(QWidget):
if process.tip_roles: if process.tip_roles:
for iii, tip_role in enumerate(process.tip_roles): for iii, tip_role in enumerate(process.tip_roles):
widget = QComboBox() widget = QComboBox()
tip_choices = [item.name for item in tip_role.instances] tip_choices = [item.name for item in tip_role.controls]
widget.setEditable(False) widget.setEditable(False)
widget.addItems(tip_choices) widget.addItems(tip_choices)
widget.setObjectName(f"tips_{tip_role.name}") widget.setObjectName(f"tips_{tip_role.name}")

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 BasicSubmission from backend.db.models import BasicRun
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: BasicSubmission): def __init__(self, parent, img_path: str | Path, submission: BasicRun):
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

@@ -1,5 +1,5 @@
""" """
Webview to show submission and sample details. Webview to show run and sample details.
""" """
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit, QGridLayout) QDialogButtonBox, QTextEdit, QGridLayout)
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType, Equipment, Process, Tips from backend.db.models import BasicRun, BasicSample, Reagent, KitType, Equipment, Process, Tips
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
from .functions import select_save_file, save_pdf from .functions import select_save_file, save_pdf
from pathlib import Path from pathlib import Path
@@ -23,10 +23,10 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionDetails(QDialog): class SubmissionDetails(QDialog):
""" """
a window showing text details of submission a window showing text details of run
""" """
def __init__(self, parent, sub: BasicSubmission | BasicSample | Reagent) -> None: def __init__(self, parent, sub: BasicRun | BasicSample | Reagent) -> None:
super().__init__(parent) super().__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -51,8 +51,8 @@ class SubmissionDetails(QDialog):
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject('backend', self) self.channel.registerObject('backend', self)
match sub: match sub:
case BasicSubmission(): case BasicRun():
self.submission_details(submission=sub) self.run_details(run=sub)
self.rsl_plate_num = sub.rsl_plate_num self.rsl_plate_num = sub.rsl_plate_num
case BasicSample(): case BasicSample():
self.sample_details(sample=sub) self.sample_details(sample=sub)
@@ -203,52 +203,52 @@ class SubmissionDetails(QDialog):
logger.error(f"Reagent with lot {old_lot} not found.") logger.error(f"Reagent with lot {old_lot} not found.")
@pyqtSlot(str) @pyqtSlot(str)
def submission_details(self, submission: str | BasicSubmission): def run_details(self, run: str | BasicRun):
""" """
Sets details view to summary of Submission. Sets details view to summary of Submission.
Args: Args:
submission (str | BasicSubmission): Submission of interest. run (str | BasicRun): Submission of interest.
""" """
logger.debug(f"Submission details.") logger.debug(f"Submission details.")
if isinstance(submission, str): if isinstance(run, str):
submission = BasicSubmission.query(rsl_plate_num=submission) run = BasicRun.query(rsl_plate_num=run)
self.rsl_plate_num = submission.rsl_plate_num self.rsl_plate_num = run.rsl_plate_num
self.base_dict = submission.to_dict(full_data=True) self.base_dict = run.to_dict(full_data=True)
# NOTE: don't want id # NOTE: don't want id
self.base_dict['platemap'] = submission.make_plate_map(sample_list=submission.hitpicked) self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked)
self.base_dict['excluded'] = submission.get_default_info("details_ignore") self.base_dict['excluded'] = run.get_default_info("details_ignore")
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict) self.base_dict, self.template = run.get_details_template(base_dict=self.base_dict)
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
# logger.debug(f"Base dictionary of submission {self.rsl_plate_num}: {pformat(self.base_dict)}") # logger.debug(f"Base dictionary of run {self.rsl_plate_num}: {pformat(self.base_dict)}")
self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css) self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html) self.webview.setHtml(self.html)
@pyqtSlot(str) @pyqtSlot(str)
def sign_off(self, submission: str | BasicSubmission) -> None: def sign_off(self, run: str | BasicRun) -> None:
""" """
Allows power user to signify a submission is complete. Allows power user to signify a run is complete.
Args: Args:
submission (str | BasicSubmission): Submission to be completed run (str | BasicRun): Submission to be completed
Returns: Returns:
None None
""" """
logger.info(f"Signing off on {submission} - ({getuser()})") logger.info(f"Signing off on {run} - ({getuser()})")
if isinstance(submission, str): if isinstance(run, str):
submission = BasicSubmission.query(rsl_plate_num=submission) run = BasicRun.query(rsl_plate_num=run)
submission.signed_by = getuser() run.signed_by = getuser()
submission.completed_date = datetime.now() run.completed_date = datetime.now()
submission.completed_date.replace(tzinfo=timezone) run.completed_date.replace(tzinfo=timezone)
submission.save() run.save()
self.submission_details(submission=self.rsl_plate_num) self.run_details(run=self.rsl_plate_num)
def save_pdf(self): def save_pdf(self):
""" """
Renders submission to html, then creates and saves .pdf file to user selected file. Renders run to html, then creates and saves .pdf file to user selected file.
""" """
fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf") fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf")
save_pdf(obj=self.webview, filename=fname) save_pdf(obj=self.webview, filename=fname)
@@ -256,10 +256,10 @@ class SubmissionDetails(QDialog):
class SubmissionComment(QDialog): class SubmissionComment(QDialog):
""" """
a window for adding comment text to a submission a window for adding comment text to a run
""" """
def __init__(self, parent, submission: BasicSubmission) -> None: def __init__(self, parent, submission: BasicRun) -> None:
super().__init__(parent) super().__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -282,7 +282,7 @@ class SubmissionComment(QDialog):
def parse_form(self) -> List[dict]: def parse_form(self) -> List[dict]:
""" """
Adds comment to submission object. Adds comment to run object.
""" """
commenter = getuser() commenter = getuser()
comment = self.txt_editor.toPlainText() comment = self.txt_editor.toPlainText()

View File

@@ -1,5 +1,5 @@
""" """
Contains widgets specific to the submission summary and submission details. Contains widgets specific to the run summary and run details.
""" """
import logging import logging
import sys import sys
@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, Q
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor
from backend.db.models import BasicSubmission, ClientSubmission from backend.db.models import BasicRun, ClientSubmission
from tools import Report, Result, report_result from tools import Report, Result, report_result
from .functions import select_open_file from .functions import select_open_file
@@ -63,7 +63,7 @@ class pandasModel(QAbstractTableModel):
class SubmissionsSheet(QTableView): class SubmissionsSheet(QTableView):
""" """
presents submission summary to user in tab1 presents run summary to user in tab1
""" """
def __init__(self, parent) -> None: def __init__(self, parent) -> None:
@@ -74,20 +74,20 @@ class SubmissionsSheet(QTableView):
page_size = self.app.page_size page_size = self.app.page_size
except AttributeError: except AttributeError:
page_size = 250 page_size = 250
self.setData(page=1, page_size=page_size) self.set_data(page=1, page_size=page_size)
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self)) self.doubleClicked.connect(lambda x: BasicRun.query(id=x.sibling(x.row(), 0).data()).show_details(self))
# NOTE: Have to run native query here because mine just returns results? # NOTE: Have to run native query here because mine just returns results?
self.total_count = BasicSubmission.__database_session__.query(BasicSubmission).count() self.total_count = BasicRun.__database_session__.query(BasicRun).count()
def setData(self, page: int = 1, page_size: int = 250) -> None: def set_data(self, page: int = 1, page_size: int = 250) -> None:
""" """
sets data in model sets data in model
""" """
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size) # self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = BasicSubmission.submissions_to_df(page=page, page_size=page_size) self.data = BasicRun.submissions_to_df(page=page, page_size=page_size)
try: try:
self.data['Id'] = self.data['Id'].apply(str) self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4) self.data['Id'] = self.data['Id'].str.zfill(4)
@@ -108,7 +108,7 @@ class SubmissionsSheet(QTableView):
id = self.selectionModel().currentIndex() id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0) # NOTE: Convert to data in id column (i.e. column 0)
id = id.sibling(id.row(), 0).data() id = id.sibling(id.row(), 0).data()
submission = BasicSubmission.query(id=id) submission = BasicRun.query(id=id)
self.menu = QMenu(self) self.menu = QMenu(self)
self.con_actions = submission.custom_context_events() self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys(): for k in self.con_actions.keys():
@@ -167,8 +167,8 @@ class SubmissionsSheet(QTableView):
for ii in range(6, len(run)): for ii in range(6, len(run)):
new_run[f"column{str(ii - 5)}_vol"] = run[ii] new_run[f"column{str(ii - 5)}_vol"] = run[ii]
# NOTE: Lookup imported submissions # NOTE: Lookup imported submissions
sub = BasicSubmission.query(rsl_plate_num=new_run['rsl_plate_num']) sub = BasicRun.query(rsl_plate_num=new_run['rsl_plate_num'])
# NOTE: If no such submission exists, move onto the next run # NOTE: If no such run exists, move onto the next run
if sub is None: if sub is None:
continue continue
try: try:
@@ -192,7 +192,7 @@ class SubmissionsSheet(QTableView):
def link_pcr_function(self): def link_pcr_function(self):
""" """
Link PCR data from run logs to an imported submission Link PCR data from run logs to an imported run
Args: Args:
obj (QMainWindow): original app window obj (QMainWindow): original app window
@@ -215,9 +215,9 @@ class SubmissionsSheet(QTableView):
experiment_name=run[4].strip(), experiment_name=run[4].strip(),
end_time=run[5].strip() end_time=run[5].strip()
) )
# NOTE: lookup imported submission # NOTE: lookup imported run
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num']) sub = BasicRun.query(rsl_number=new_run['rsl_plate_num'])
# NOTE: if imported submission doesn't exist move on to next run # NOTE: if imported run doesn't exist move on to next run
if sub is None: if sub is None:
continue continue
sub.set_attribute('pcr_info', new_run) sub.set_attribute('pcr_info', new_run)
@@ -302,7 +302,7 @@ class SubmissionsTree(QTreeView):
id = int(id.data()) id = int(id.data())
except ValueError: except ValueError:
return return
BasicSubmission.query(id=id).show_details(self) BasicRun.query(id=id).show_details(self)
def link_extractions(self): def link_extractions(self):

View File

@@ -1,5 +1,5 @@
""" """
Contains all submission related frontend functions Contains all run related frontend functions
""" """
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
@@ -10,11 +10,11 @@ from .functions import select_open_file, select_save_file
import logging import logging
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.excel.parsers import SheetParser, InfoParserV2 from backend.excel import SheetParser, InfoParser
from backend.validators import PydSubmission, PydReagent from backend.validators import PydSubmission, PydReagent
from backend.db import ( from backend.db import (
Organization, SubmissionType, Reagent, Organization, SubmissionType, Reagent,
ReagentRole, KitTypeReagentRoleAssociation, BasicSubmission ReagentRole, KitTypeReagentRoleAssociation, BasicRun
) )
from pprint import pformat from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop from .pop_ups import QuestionAsker, AlertPop
@@ -93,7 +93,7 @@ class SubmissionFormContainer(QWidget):
@report_result @report_result
def import_submission_function(self, fname: Path | None = None) -> Report: def import_submission_function(self, fname: Path | None = None) -> Report:
""" """
Import a new submission to the app window Import a new run to the app window
Args: Args:
obj (QMainWindow): original app window obj (QMainWindow): original app window
@@ -122,12 +122,12 @@ class SubmissionFormContainer(QWidget):
# NOTE: create sheetparser using excel sheet and context from gui # NOTE: create sheetparser using excel sheet and context from gui
try: try:
# self.prsr = SheetParser(filepath=fname) # self.prsr = SheetParser(filepath=fname)
self.parser = InfoParserV2(filepath=fname) self.parser = InfoParser(filepath=fname)
except PermissionError: except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}") logger.error(f"Couldn't get permission to access file: {fname}")
return return
except AttributeError: except AttributeError:
self.parser = InfoParserV2(filepath=fname) self.parser = InfoParser(filepath=fname)
self.pyd = self.parser.to_pydantic() self.pyd = self.parser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pyd.samples)}") # logger.debug(f"Samples: {pformat(self.pyd.samples)}")
checker = SampleChecker(self, "Sample Checker", self.pyd) checker = SampleChecker(self, "Sample Checker", self.pyd)
@@ -150,7 +150,7 @@ class SubmissionFormContainer(QWidget):
instance (Reagent | None): Blank reagent instance to be edited and then added. instance (Reagent | None): Blank reagent instance to be edited and then added.
Returns: Returns:
models.Reagent: the constructed reagent object to add to submission models.Reagent: the constructed reagent object to add to run
""" """
report = Report() report = Report()
if not instance: if not instance:
@@ -178,7 +178,7 @@ class SubmissionFormWidget(QWidget):
self.missing_info = [] self.missing_info = []
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value']) self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
basic_submission_class = self.submission_type.submission_class basic_submission_class = self.submission_type.submission_class
logger.debug(f"Basic submission class: {basic_submission_class}") logger.debug(f"Basic run class: {basic_submission_class}")
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value']) defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
self.recover = defaults['form_recover'] self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore'] self.ignore = defaults['form_ignore']
@@ -202,7 +202,7 @@ class SubmissionFormWidget(QWidget):
value = dict(value=None, missing=True) value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}") logger.debug(f"Pydantic value: {value}")
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type, add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
sub_obj=basic_submission_class, disable=check) run_object=basic_submission_class, disable=check)
if add_widget is not None: if add_widget is not None:
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
if k in self.__class__.update_reagent_fields: if k in self.__class__.update_reagent_fields:
@@ -223,7 +223,7 @@ class SubmissionFormWidget(QWidget):
reagent.flip_check(self.disabler.checkbox.isChecked()) reagent.flip_check(self.disabler.checkbox.isChecked())
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None, def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None,
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None, extraction_kit: str | None = None, run_object: BasicRun | None = None,
disable: bool = False) -> "self.InfoItem": disable: bool = False) -> "self.InfoItem":
""" """
Make an InfoItem widget to hold a field Make an InfoItem widget to hold a field
@@ -248,7 +248,7 @@ class SubmissionFormWidget(QWidget):
widget = None widget = None
case _: case _:
widget = self.InfoItem(parent=self, key=key, value=value, submission_type=submission_type, widget = self.InfoItem(parent=self, key=key, value=value, submission_type=submission_type,
sub_obj=sub_obj) run_object=run_object)
if disable: if disable:
widget.input.setEnabled(False) widget.input.setEnabled(False)
widget.input.setToolTip("Widget disabled to protect database integrity.") widget.input.setToolTip("Widget disabled to protect database integrity.")
@@ -373,14 +373,14 @@ class SubmissionFormWidget(QWidget):
return report return report
case _: case _:
pass pass
# NOTE: add reagents to submission object # NOTE: add reagents to run object
if base_submission is None: if base_submission is None:
return return
for reagent in base_submission.reagents: for reagent in base_submission.reagents:
reagent.update_last_used(kit=base_submission.extraction_kit) reagent.update_last_used(kit=base_submission.extraction_kit)
save_output = base_submission.save() save_output = base_submission.save()
# NOTE: update summary sheet # NOTE: update summary sheet
self.app.table_widget.sub_wid.setData() self.app.table_widget.sub_wid.set_data()
# NOTE: reset form # NOTE: reset form
try: try:
check = save_output.results == [] check = save_output.results == []
@@ -393,7 +393,7 @@ class SubmissionFormWidget(QWidget):
def export_csv_function(self, fname: Path | None = None): def export_csv_function(self, fname: Path | None = None):
""" """
Save the submission's csv file. Save the run's csv file.
Args: Args:
fname (Path | None, optional): Input filename. Defaults to None. fname (Path | None, optional): Input filename. Defaults to None.
@@ -405,7 +405,7 @@ class SubmissionFormWidget(QWidget):
except PermissionError: except PermissionError:
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.") logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
except AttributeError: except AttributeError:
logger.error(f"No csv file found in the submission at this point.") logger.error(f"No csv file found in the run at this point.")
def parse_form(self) -> Report: def parse_form(self) -> Report:
""" """
@@ -446,14 +446,14 @@ class SubmissionFormWidget(QWidget):
class InfoItem(QWidget): class InfoItem(QWidget):
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None, def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> None: run_object: BasicRun | None = None) -> None:
super().__init__(parent) super().__init__(parent)
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
layout = QVBoxLayout() layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value) self.label = self.ParsedQLabel(key=key, value=value)
self.input: QWidget = self.set_widget(parent=parent, key=key, value=value, submission_type=submission_type, self.input: QWidget = self.set_widget(parent=parent, key=key, value=value, submission_type=submission_type,
sub_obj=sub_obj) sub_obj=run_object)
self.setObjectName(key) self.setObjectName(key)
try: try:
self.missing: bool = value['missing'] self.missing: bool = value['missing']
@@ -492,7 +492,7 @@ class SubmissionFormWidget(QWidget):
def set_widget(self, parent: QWidget, key: str, value: dict, def set_widget(self, parent: QWidget, key: str, value: dict,
submission_type: str | SubmissionType | None = None, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> QWidget: sub_obj: BasicRun | None = None) -> QWidget:
""" """
Creates form widget Creates form widget
@@ -568,7 +568,7 @@ class SubmissionFormWidget(QWidget):
except ValueError: except ValueError:
categories.insert(0, categories.pop(categories.index(submission_type))) categories.insert(0, categories.pop(categories.index(submission_type)))
add_widget.addItems(categories) add_widget.addItems(categories)
add_widget.setToolTip("Enter submission category or select from list.") add_widget.setToolTip("Enter run category or select from list.")
case _: case _:
if key in sub_obj.timestamps: if key in sub_obj.timestamps:
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent) add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
@@ -692,7 +692,7 @@ class SubmissionFormWidget(QWidget):
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent) wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
return wanted_reagent, report return wanted_reagent, report
else: else:
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check # NOTE: In this case we will have an empty reagent and the run will fail kit integrity check
return None, report return None, report
else: else:
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly. # NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
@@ -801,7 +801,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
Report: Report on status of parse. Report: Report on status of parse.
""" """
report = Report() report = Report()
logger.info(f"Hello from client submission form parser!") logger.info(f"Hello from client run form parser!")
info = {} info = {}
reagents = [] reagents = []
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):

View File

@@ -43,7 +43,7 @@ class Summary(InfoPane):
orgs = self.org_select.get_checked() orgs = self.org_select.get_checked()
self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs) self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
self.webview.setHtml(self.report_obj.html) self.webview.setHtml(self.report_obj.html)
if self.report_obj.subs: if self.report_obj.runs:
self.save_pdf_button.setEnabled(True) self.save_pdf_button.setEnabled(True)
self.save_excel_button.setEnabled(True) self.save_excel_button.setEnabled(True)
else: else: