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:
logger.error(f"Error, could not import from irida due to {e}")
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 " \
f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})"
cursor = conn.execute(sql)
@@ -57,7 +57,7 @@ def import_irida(ctx: Settings):
except IndexError:
logger.error(f"Could not get sample for {sample}")
instance.submission = None
# instance.submission = sample.submission[0]
# instance.run = sample.run[0]
new_session.add(instance)
new_session.commit()
new_session.close()

View File

@@ -33,11 +33,13 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
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_inheritable = []
searchables = []
misc_info = Column(JSON)
def __repr__(self) -> str:
try:
return f"<{self.__class__.__name__}({self.name})>"
@@ -120,6 +122,26 @@ class BaseClass(Base):
from test_settings import ctx
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
def get_default_info(cls, *args) -> dict | list | str:
"""
@@ -150,7 +172,6 @@ class BaseClass(Base):
else:
return cls.__subclasses__()
@classmethod
def fuzzy_search(cls, **kwargs) -> List[Any]:
"""
@@ -177,7 +198,7 @@ class BaseClass(Base):
@classmethod
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:
objects (list): Objects to be converted to dataframe.
@@ -519,12 +540,13 @@ class ConfigItem(BaseClass):
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 .runs import *
from .kits import *
from .submissions import *
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
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
name = Column(String(255), unique=True) #: controltype name (e.g. Irida Control)
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
@setup_lookup
@@ -64,11 +64,11 @@ class ControlType(BaseClass):
Returns:
List[str]: list of subtypes available
"""
if not self.instances:
if not self.controls:
return
# NOTE: Get first instance since all should have same subtypes
# NOTE: Get mode of instance
jsoner = getattr(self.instances[0], mode)
jsoner = getattr(self.controls[0], mode)
try:
# NOTE: Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0]
@@ -119,17 +119,17 @@ class Control(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key
controltype_name = Column(String, ForeignKey("_controltype.name", ondelete="SET NULL",
name="fk_BC_subtype_name")) #: name of joined submission type
controltype = relationship("ControlType", back_populates="instances",
name="fk_BC_subtype_name")) #: name of joined run type
controltype = relationship("ControlType", back_populates="controls",
foreign_keys=[controltype_name]) #: reference to parent control type
name = Column(String(255), unique=True) #: Sample ID
sample_id = Column(String, ForeignKey("_basicsample.id", ondelete="SET NULL",
name="fk_Cont_sample_id")) #: name of joined submission type
sample = relationship("BasicSample", back_populates="control") #: This control's submission sample
name="fk_Cont_sample_id")) #: name of joined run type
sample = relationship("BasicSample", back_populates="control") #: This control's run sample
submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id")) #: parent submission id
submission = relationship("BasicSubmission", back_populates="controls",
foreign_keys=[submission_id]) #: parent submission
procedure_id = Column(INTEGER, ForeignKey("_procedure.id")) #: parent run id
procedure = relationship("Procedure", back_populates="controls",
foreign_keys=[procedure_id]) #: parent run
__mapper_args__ = {
"polymorphic_identity": "Basic Control",
@@ -147,7 +147,7 @@ class Control(BaseClass):
@classmethod
@setup_lookup
def query(cls,
submissiontype: str | None = None,
proceduretype: str | None = None,
subtype: str | None = None,
start_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.
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.
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.
@@ -168,15 +168,15 @@ class Control(BaseClass):
Returns:
Control|List[Control]: Control object of interest.
"""
from backend.db import SubmissionType
from backend.db import ProcedureType
query: Query = cls.__database_session__.query(cls)
match submissiontype:
match proceduretype:
case str():
from backend import BasicSubmission, SubmissionType
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submissiontype)
case SubmissionType():
from backend import BasicSubmission
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submissiontype.name)
from backend.db import Procedure
query = query.join(Procedure).join(ProcedureType).filter(ProcedureType.name == proceduretype)
case ProcedureType():
from backend import Procedure
query = query.join(Procedure).filter(Procedure.submission_type_name == proceduretype.name)
case _:
pass
# NOTE: by control type
@@ -234,13 +234,13 @@ class Control(BaseClass):
model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_
except Exception as e:
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():
try:
model = cls.__mapper__.polymorphic_map[polymorphic_identity.name].class_
except Exception as e:
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 _:
pass
# 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.setEnabled(False)
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'])
data = [control.to_sub_dict() for control in controls]
df = DataFrame.from_records(data)
@@ -402,7 +402,7 @@ class IridaControl(Control):
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:
dict: output dictionary containing: Name, Type, Targets, Top Kraken results
@@ -565,7 +565,7 @@ class IridaControl(Control):
Args:
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:
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 pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, Organization, LogMixin
from . import Base, BaseClass, Organization, LogMixin, ProcedureType
from io import BytesIO
logger = logging.getLogger(f'submissions.{__name__}')
@@ -94,10 +94,10 @@ equipment_tips = Table(
extend_existing=True
)
kittypes_submissions = Table(
"_kittypes_submissions",
kittypes_runs = Table(
"_kittypes_runs",
Base.metadata,
Column("_basicsubmission_id", INTEGER, ForeignKey("_basicsubmission.id")),
Column("_basicrun_id", INTEGER, ForeignKey("_basicrun.id")),
Column("kittype_id", INTEGER, ForeignKey("_kittype.id")),
extend_existing=True
)
@@ -105,15 +105,15 @@ kittypes_submissions = Table(
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"]
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="kittypes",
secondary=kittypes_submissions) #: submissions this kit was used for
runs = relationship("BasicRun", back_populates="kittypes",
secondary=kittypes_runs) #: runs this kit was used for
processes = relationship("Process", back_populates="kit_types",
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}")
assocs = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type]
# logger.debug(f"Associations: {assocs}")
# NOTE: rescue with submission type's default kit.
# NOTE: rescue with run type's default kit.
if not assocs:
logger.error(
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
dlg = ObjectSelector(
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__,
values=[kit.name for kit in submission_type.kit_types]
)
@@ -378,7 +378,7 @@ class KitType(BaseClass):
# if not new_role:
# new_role = EquipmentRole(name=role['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,
# equipment_role=new_role)
# try:
@@ -425,7 +425,7 @@ class ReagentRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of role reagent plays
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
reagentrole_kit_associations = relationship(
@@ -548,7 +548,7 @@ class Reagent(BaseClass, LogMixin):
"""
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
role_id = Column(INTEGER, ForeignKey("_reagentrole.id", ondelete='SET NULL',
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
reagent_submission_associations = relationship(
"SubmissionReagentAssociation",
"RunReagentAssociation",
back_populates="reagent",
cascade="all, delete-orphan",
) #: Relation to SubmissionSampleAssociation
submissions = association_proxy("reagent_submission_associations", "submission",
creator=lambda sub: SubmissionReagentAssociation(
submissions = association_proxy("reagent_submission_associations", "run",
creator=lambda sub: RunReagentAssociation(
submission=sub)) #: Association proxy to SubmissionSampleAssociation.samples
def __repr__(self):
@@ -845,10 +845,10 @@ class SubmissionType(BaseClass):
"""
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.
defaults = Column(JSON) #: Basic information about this submission type
instances = relationship("ClientSubmission", back_populates="submission_type") #: Concrete instances of this type.
defaults = Column(JSON) #: Basic information about this run 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.
processes = relationship("Process", back_populates="submission_types",
secondary=submissiontypes_processes) #: Relation to equipment processes used for this type.
@@ -1107,7 +1107,7 @@ class SubmissionType(BaseClass):
@classproperty
def omni_removes(cls):
return super().omni_removes + ["defaults", "instances"]
return super().omni_removes + ["defaults", "controls"]
@classproperty
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]))
@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:
BasicSubmission: Submission class
BasicRun: Submission class
"""
from .submissions import BasicSubmission
return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name)
from .submissions import BasicRun
return BasicRun.find_polymorphic_subclass(polymorphic_identity=self.name)
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[SubmissionType, bool]:
@@ -1264,7 +1264,7 @@ class SubmissionType(BaseClass):
new = True
for k, v in sanitized_kwargs.items():
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
@classmethod
@@ -1276,10 +1276,10 @@ class SubmissionType(BaseClass):
**kwargs
) -> 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:
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.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -1319,7 +1319,7 @@ class SubmissionType(BaseClass):
@check_authorization
def save(self):
"""
Adds this instances to the database and commits.
Adds this controls to the database and commits.
"""
super().save()
@@ -1399,29 +1399,29 @@ class SubmissionType(BaseClass):
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_sort = ["submission_type", "kit_type"]
omni_removes = BaseClass.omni_removes + ["procedure_type_id", "procedure_id"]
omni_sort = ["proceduretype", "kittype"]
level = 2
submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
primary_key=True) #: id of joined submission type
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
primary_key=True) #: id of joined run type
kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
mutable_cost_column = Column(
FLOAT(2)) #: dollar amount per 96 well plate that can change with number of columns (reagents, tips, etc)
mutable_cost_sample = Column(
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)
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
submission_type = relationship(SubmissionType,
back_populates="submissiontype_kit_associations") #: joined submission type
proceduretype = relationship(ProcedureType,
back_populates="proceduretype_kittype_associations") #: joined run type
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):
@@ -1495,7 +1495,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
Lookup SubmissionTypeKitTypeAssociations of interest.
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.
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
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)
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)
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)
else:
v = v.instance_object
case "submissiontype" | "submission_type":
case "proceduretype" | "submission_type":
k = "submission_type"
if isinstance(v, str):
v = SubmissionType.query(name=v)
@@ -1742,18 +1742,6 @@ class KitTypeReagentRoleAssociation(BaseClass):
limit = 1
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]:
"""
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:
Generator: Generates of reagents.
"""
reagents = self.reagent_role.instances
reagents = self.reagent_role.controls
try:
regex = self.uses['exclude_regex']
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
"""
skip_on_edit = True
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
submission = relationship("BasicSubmission",
back_populates="submission_reagent_associations") #: associated submission
procedure = relationship("Procedure",
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:
"""
Returns:
str: Representation of this SubmissionReagentAssociation
str: Representation of this RunReagentAssociation
"""
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:
logger.error(f"Reagent {self.reagent.lot} submission association {self.reagent_id} has no submissions!")
return f"<SubmissionReagentAssociation(Unknown Submission & {self.reagent.lot})>"
logger.error(f"Reagent {self.reagent.lot} run association {self.reagent_id} has no submissions!")
return f"<RunReagentAssociation(Unknown Submission & {self.reagent.lot})>"
def __init__(self, reagent=None, submission=None):
if isinstance(reagent, list):
@@ -1860,21 +1848,21 @@ class SubmissionReagentAssociation(BaseClass):
@classmethod
@setup_lookup
def query(cls,
submission: "BasicSubmission" | str | int | None = None,
run: "BasicRun" | str | int | None = None,
reagent: Reagent | str | None = None,
limit: int = 0) -> SubmissionReagentAssociation | List[SubmissionReagentAssociation]:
limit: int = 0) -> RunReagentAssociation | List[RunReagentAssociation]:
"""
Lookup SubmissionReagentAssociations of interest.
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.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
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)
match reagent:
case Reagent() | str():
@@ -1883,27 +1871,27 @@ class SubmissionReagentAssociation(BaseClass):
query = query.filter(cls.reagent == reagent)
case _:
pass
match submission:
case BasicSubmission() | str():
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
query = query.filter(cls.submission == submission)
match run:
case BasicRun() | str():
if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run)
query = query.filter(cls.run == run)
case int():
submission = BasicSubmission.query(id=submission)
query = query.join(BasicSubmission).filter(BasicSubmission.id == submission)
run = BasicRun.query(id=run)
query = query.join(BasicRun).filter(BasicRun.id == run)
case _:
pass
return cls.execute_query(query=query, limit=limit)
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:
extraction_kit (_type_): Extraction kit of interest
Returns:
dict: This SubmissionReagentAssociation as dict
dict: This RunReagentAssociation as dict
"""
output = self.reagent.to_sub_dict(extraction_kit)
output['comments'] = self.comments
@@ -1923,20 +1911,20 @@ class Equipment(BaseClass, LogMixin):
name = Column(String(64)) #: equipment name
nickname = Column(String(64)) #: equipment nickname
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
processes = relationship("Process", back_populates="equipment",
secondary=equipment_processes) #: relation to Processes
tips = relationship("Tips", back_populates="equipment",
secondary=equipment_tips) #: relation to Processes
equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation",
"RunEquipmentAssociation",
back_populates="equipment",
cascade="all, delete-orphan",
) #: Association with BasicSubmission
) #: Association with BasicRun
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:
"""
@@ -2135,7 +2123,7 @@ class EquipmentRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: Role id, primary key
name = Column(String(32)) #: Common name
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',
secondary=equipmentroles_processes) #: Associated Processes
@@ -2253,13 +2241,13 @@ class EquipmentRole(BaseClass):
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
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
process_id = Column(INTEGER, ForeignKey("_process.id", ondelete="SET NULL",
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
comments = Column(String(1024)) #: comments about equipment
submission = relationship("BasicSubmission",
back_populates="submission_equipment_associations") #: associated submission
run = relationship("BasicRun",
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:
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"):
self.submission = submission
def __init__(self, run, equipment, role: str = "None"):
self.run = run
self.equipment = equipment
self.role = role
@@ -2286,10 +2274,10 @@ class SubmissionEquipmentAssociation(BaseClass):
def to_sub_dict(self) -> dict:
"""
This SubmissionEquipmentAssociation as a dictionary
This RunEquipmentAssociation as a dictionary
Returns:
dict: This SubmissionEquipmentAssociation as a dictionary
dict: This RunEquipmentAssociation as a dictionary
"""
try:
process = self.process.name
@@ -2311,12 +2299,12 @@ class SubmissionEquipmentAssociation(BaseClass):
@classmethod
@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) \
-> Any | List[Any]:
query: Query = cls.__database_session__.query(cls)
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:
query = query.filter(cls.role == role)
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
submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
primary_key=True) #: id of associated submission
uses = Column(JSON) #: locations of equipment on the submission type excel sheet.
primary_key=True) #: id of associated run
uses = Column(JSON) #: locations of equipment on the run type excel sheet.
static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
submission_type = relationship(SubmissionType,
back_populates="submissiontype_equipmentrole_associations") #: associated submission
back_populates="submissiontype_equipmentrole_associations") #: associated run
equipment_role = relationship(EquipmentRole,
back_populates="equipmentrole_submissiontype_associations") #: associated equipment
@@ -2386,8 +2374,8 @@ class Process(BaseClass):
secondary=equipment_processes) #: relation to Equipment
equipment_roles = relationship("EquipmentRole", back_populates='processes',
secondary=equipmentroles_processes) #: relation to EquipmentRoles
submissions = relationship("SubmissionEquipmentAssociation",
backref='process') #: relation to SubmissionEquipmentAssociation
submissions = relationship("RunEquipmentAssociation",
backref='process') #: relation to RunEquipmentAssociation
kit_types = relationship("KitType", back_populates='processes',
secondary=kittypes_processes) #: relation to KitType
tip_roles = relationship("TipRole", back_populates='processes',
@@ -2545,14 +2533,14 @@ class TipRole(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type
instances = relationship("Tips", back_populates="role",
secondary=tiproles_tips) #: concrete instances of this reagent type
secondary=tiproles_tips) #: concrete controls of this reagent type
processes = relationship("Process", back_populates="tip_roles", secondary=process_tiprole)
tiprole_submissiontype_associations = relationship(
"SubmissionTypeTipRoleAssociation",
back_populates="tip_role",
cascade="all, delete-orphan"
) #: associated submission
) #: associated run
submission_types = association_proxy("tiprole_submissiontype_associations", "submission_type")
@@ -2608,21 +2596,21 @@ class Tips(BaseClass, LogMixin):
A concrete instance of tips.
"""
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
role_id = Column(INTEGER, ForeignKey("_tiprole.id", ondelete='SET NULL',
name="fk_tip_role_id")) #: id of parent reagent type
name = Column(String(64)) #: tip common name
lot = Column(String(64)) #: lot number of tips
equipment = relationship("Equipment", back_populates="tips",
secondary=equipment_tips) #: associated submission
secondary=equipment_tips) #: associated run
tips_submission_associations = relationship(
"SubmissionTipsAssociation",
back_populates="tips",
cascade="all, delete-orphan"
) #: associated submission
) #: associated run
submissions = association_proxy("tips_submission_associations", 'submission')
submissions = association_proxy("tips_submission_associations", 'run')
@hybrid_property
def tiprole(self):
@@ -2728,12 +2716,12 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
"""
tiprole_id = Column(INTEGER, ForeignKey("_tiprole.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
primary_key=True) #: id of associated submission
uses = Column(JSON) #: locations of equipment on the submission type excel sheet.
primary_key=True) #: id of associated run
uses = Column(JSON) #: locations of equipment on the run type excel sheet.
static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
submission_type = relationship(SubmissionType,
back_populates="submissiontype_tiprole_associations") #: associated submission
back_populates="submissiontype_tiprole_associations") #: associated run
tip_role = relationship(TipRole,
back_populates="tiprole_submissiontype_associations") #: associated equipment
@@ -2753,16 +2741,16 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
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
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id"), primary_key=True) #: id of associated submission
submission = relationship("BasicSubmission",
back_populates="submission_tips_associations") #: associated submission
run_id = Column(INTEGER, ForeignKey("_basicrun.id"), primary_key=True) #: id of associated run
run = relationship("BasicRun",
back_populates="run_tips_associations") #: associated run
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"))
def to_sub_dict(self) -> dict:
@@ -2776,21 +2764,21 @@ class SubmissionTipsAssociation(BaseClass):
@classmethod
@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]:
query: Query = cls.__database_session__.query(cls)
query = query.filter(cls.tip_id == tip_id)
if submission_id is not None:
query = query.filter(cls.submission_id == submission_id)
if run_id is not None:
query = query.filter(cls.run_id == run_id)
query = query.filter(cls.role_name == role)
return cls.execute_query(query=query, limit=limit, **kwargs)
@classmethod
def query_or_create(cls, tips, submission, role: str, **kwargs):
def query_or_create(cls, tips, run, role: str, **kwargs):
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:
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
instance = cls(run=run, tips=tips, role_name=role)
return instance
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 .submission_parser import *
from .reports 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
from copy import copy
@@ -45,8 +45,8 @@ class SheetParser(object):
self.sub['submission_type'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath),
missing=True)
self.submission_type = SubmissionType.query(name=self.sub['submission_type'])
self.sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
# NOTE: grab the info map from the submission type in database
self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
# NOTE: grab the info map from the run type in database
self.parse_info()
self.import_kit_validation_check()
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)
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:
check = parser.parsed_info['submission_type']['value'] not in [None, "None", "", " "]
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
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 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'])
logger.info(f"Updated self.submission_type to {self.submission_type}. Rerunning parse.")
self.parse_info()
@@ -145,17 +145,17 @@ class InfoParser(object):
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:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
submission_type (str | SubmissionType): Type of run expected (Wastewater, Bacterial Culture, etc.)
sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None.
"""
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
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 = dict(value=self.submission_type_obj.name, missing=True)
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.
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")
@property
@@ -232,7 +232,7 @@ class InfoParser(object):
dicto[item['name']] = dict(value=value, missing=missing)
except (KeyError, IndexError):
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'])
@@ -242,20 +242,20 @@ class ReagentParser(object):
"""
def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str,
sub_object: BasicSubmission | None = None):
run_object: BasicRun | None = None):
"""
Args:
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.
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):
submission_type = SubmissionType.query(name=submission_type)
self.submission_type_obj = submission_type
if not sub_object:
sub_object = submission_type.submission_class
self.sub_object = sub_object
if not run_object:
run_object = submission_type.submission_class
self.run_object = run_object
if isinstance(extraction_kit, dict):
extraction_kit = extraction_kit['value']
self.kit_object = KitType.query(name=extraction_kit)
@@ -267,7 +267,7 @@ class ReagentParser(object):
Gets location of kit reagents from database
Args:
submission_type (str): Name of submission type.
submission_type (str): Name of run type.
Returns:
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,
sub_object: BasicSubmission | None = None) -> None:
sub_object: BasicRun | None = None) -> None:
"""
Args:
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.
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.xl = xl
@@ -343,8 +343,8 @@ class SampleParser(object):
self.submission_type_obj = submission_type
if sub_object is None:
logger.warning(
f"Sample parser attempting to fetch submission class with polymorphic identity: {self.submission_type}")
sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
f"Sample parser attempting to fetch run class with polymorphic identity: {self.submission_type}")
sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.sub_object = sub_object
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)
@@ -352,10 +352,10 @@ class SampleParser(object):
@property
def sample_map(self) -> dict:
"""
Gets info locations in excel book for submission type.
Gets info locations in excel book for run type.
Args:
submission_type (str): submission type
submission_type (str): run type
Returns:
dict: Info locations.
@@ -478,7 +478,7 @@ class EquipmentParser(object):
"""
Args:
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):
submission_type = SubmissionType.query(name=submission_type)
@@ -488,7 +488,7 @@ class EquipmentParser(object):
@property
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:
List[dict]: List of locations
@@ -556,7 +556,7 @@ class TipParser(object):
"""
Args:
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):
submission_type = SubmissionType.query(name=submission_type)
@@ -566,7 +566,7 @@ class TipParser(object):
@property
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:
List[dict]: List of locations
@@ -609,11 +609,11 @@ class TipParser(object):
class PCRParser(object):
"""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:
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:
logger.error('No filepath given.')
@@ -659,7 +659,7 @@ class PCRParser(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:
logger.error('No filepath given.')
self.xl = None
@@ -672,11 +672,11 @@ class ConcentrationParser(object):
except PermissionError:
logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.")
return None
if submission is None:
self.submission_obj = BacterialCulture
if run is None:
self.submission_obj = BasicRun()
rsl_plate_num = None
else:
self.submission_obj = submission
self.submission_obj = run
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)

View File

@@ -7,7 +7,7 @@ from pandas import DataFrame, ExcelWriter
from pathlib import Path
from datetime import date
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 PyQt6.QtWidgets import QWidget
from openpyxl.worksheet.worksheet import Worksheet
@@ -45,9 +45,9 @@ class ReportMaker(object):
self.start_date = start_date
self.end_date = end_date
# 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:
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.html = self.make_report_html(df=self.summary_df)
@@ -58,9 +58,9 @@ class ReportMaker(object):
Returns:
DataFrame: output dataframe
"""
if not self.subs:
if not self.runs:
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
df = df.sort_values("submitting_lab")
# NOTE: aggregate cost and sample count columns
@@ -156,19 +156,19 @@ class TurnaroundMaker(ReportArchetype):
self.start_date = start_date
self.end_date = end_date
# 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)
records = [self.build_record(sub) for sub in self.subs]
self.df = DataFrame.from_records(records)
self.sheet_name = "Turnaround"
@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:
sub (BasicSubmission): The submission to be processed.
sub (BasicRun): The run to be processed.
Returns:
@@ -203,9 +203,9 @@ class ConcentrationMaker(ReportArchetype):
self.start_date = start_date
self.end_date = end_date
# 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)
# 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.records = [self.build_record(sample) for sample in self.samples]
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
from copy import copy
@@ -8,7 +8,7 @@ from operator import itemgetter
from pprint import pformat
from typing import List, Generator, Tuple
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 io import BytesIO
from collections import OrderedDict
@@ -24,7 +24,7 @@ class SheetWriter(object):
def __init__(self, submission: PydSubmission):
"""
Args:
submission (PydSubmission): Object containing submission information.
submission (PydSubmission): Object containing run information.
"""
self.sub = OrderedDict(submission.improved_dict())
# NOTE: Set values from pydantic object.
@@ -35,7 +35,7 @@ class SheetWriter(object):
case 'submission_type':
self.sub[k] = 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)
case _:
if isinstance(v, dict):
@@ -99,22 +99,22 @@ class SheetWriter(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,
sub_object: BasicSubmission | None = None):
sub_object: BasicRun | None = None):
"""
Args:
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.
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):
submission_type = SubmissionType.query(name=submission_type)
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.sub_object = sub_object
self.xl = xl
@@ -196,7 +196,7 @@ class ReagentWriter(object):
"""
Args:
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.
reagent_list (list): List of reagent dicts to be written to excel.
"""
@@ -273,7 +273,7 @@ class SampleWriter(object):
"""
Args:
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.
"""
if isinstance(submission_type, str):
@@ -281,7 +281,7 @@ class SampleWriter(object):
self.submission_type = submission_type
self.xl = xl
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]
self.samples = sorted(samples, key=itemgetter('submission_rank'))
self.blank_lookup_table()
@@ -351,7 +351,7 @@ class EquipmentWriter(object):
"""
Args:
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.
"""
if isinstance(submission_type, str):
@@ -433,7 +433,7 @@ class TipWriter(object):
"""
Args:
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.
"""
if isinstance(submission_type, str):

View File

@@ -5,7 +5,7 @@ import logging, re
import sys
from pathlib import Path
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 jinja2 import Template
from dateutil.parser import parse
@@ -25,9 +25,9 @@ class RSLNamer(object):
self.submission_type = submission_type
if not self.submission_type:
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:
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(
submission_type=submission_type))
if not data:
@@ -40,7 +40,7 @@ class RSLNamer(object):
@classmethod
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:
filename (str | Path): filename
@@ -49,12 +49,12 @@ class RSLNamer(object):
TypeError: Raised if unsupported variable type for filename given.
Returns:
str: parsed submission type
str: parsed run type
"""
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:
filepath ():
@@ -83,13 +83,13 @@ class RSLNamer(object):
def st_from_str(file_name: str) -> str:
if file_name.startswith("tmp"):
return "Bacterial Culture"
regex = BasicSubmission.regex
regex = BasicRun.regex
m = regex.search(file_name)
try:
sub_type = m.lastgroup
except AttributeError as e:
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
match filename:
@@ -107,8 +107,8 @@ class RSLNamer(object):
if "pytest" in sys.modules:
raise ValueError("Submission Type came back as None.")
from frontend.widgets import ObjectSelector
dlg = ObjectSelector(title="Couldn't parse submission type.",
message="Please select submission type from list below.",
dlg = ObjectSelector(title="Couldn't parse run type.",
message="Please select run type from list below.",
obj_type=SubmissionType)
if dlg.exec():
submission_type = dlg.parse_form()
@@ -118,14 +118,14 @@ class RSLNamer(object):
@classmethod
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:
regex (str): string to construct pattern
filename (str): string to be parsed
"""
if regex is None:
regex = BasicSubmission.regex
regex = BasicRun.regex
match filename:
case Path():
m = regex.search(filename.stem)
@@ -145,10 +145,10 @@ class RSLNamer(object):
@classmethod
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:
data (dict): incoming submission data
data (dict): incoming run data
Returns:
str: Output filename
@@ -170,7 +170,7 @@ class RSLNamer(object):
if "rsl_plate_num" in data.keys():
plate_number = data['rsl_plate_num'].split("-")[-1][0]
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
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)
Args:
template (jinja2.Template): Template stored in BasicSubmission
template (jinja2.Template): Template stored in BasicRun
Returns:
str: output file name.

View File

@@ -270,7 +270,7 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni):
except AttributeError:
return f"<{self.__class__.__name__}(NO NAME)>"
@field_validator("submissiontype", mode="before")
@field_validator("proceduretype", mode="before")
@classmethod
def rescue_submissiontype_none(cls, value):
if not value:
@@ -324,7 +324,7 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni):
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):
submissiontype = SubmissionType.query(name=self.submissiontype.name)
else:
@@ -334,7 +334,7 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni):
else:
kittype = KitType.query(name=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.mutable_cost_column = self.mutable_cost_column
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}
@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
@@ -141,7 +141,7 @@ class PydReagent(BaseModel):
# NOTE: reagent method sets fields based on keys in dictionary
reagent.set_attribute(key, value)
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
else:
assoc = None
@@ -198,13 +198,13 @@ class PydSample(BaseModel, extra='allow'):
return value
@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]:
"""
Converts this instance into a backend.db.models.submissions.Sample object
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:
Tuple[BasicSample, Result]: Sample object and result object.
@@ -220,13 +220,13 @@ class PydSample(BaseModel, extra='allow'):
case _:
instance.__setattr__(key, value)
out_associations = []
if submission is not None:
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
assoc_type = submission.submission_type_name
if run is not None:
if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run)
assoc_type = run.submission_type_name
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",
submission=submission,
submission=run,
sample=instance,
row=row, column=column, id=aid,
submission_rank=submission_rank,
@@ -234,7 +234,7 @@ class PydSample(BaseModel, extra='allow'):
try:
out_associations.append(association)
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()
return instance, out_associations, report
@@ -267,20 +267,20 @@ class PydTips(BaseModel):
return value
@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.
Args:
submission (BasicSubmission): A submission object to associate tips represented here.
submission (BasicRun): A run object to associate tips represented here.
Returns:
SubmissionTipsAssociation: Association between queried tips and submission
SubmissionTipsAssociation: Association between queried tips and run
"""
report = Report()
tips = Tips.query(name=self.name, limit=1)
# 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
@@ -315,19 +315,19 @@ class PydEquipment(BaseModel, extra='ignore'):
return value
@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
Args:
submission ( BasicSubmission | str ): BasicSubmission of interest
submission ( BasicRun | str ): BasicRun of interest
Returns:
Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects
Tuple[Equipment, RunEquipmentAssociation]: SQL objects
"""
report = Report()
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
submission = BasicRun.query(rsl_plate_num=submission)
if isinstance(extraction_kit, str):
extraction_kit = KitType.query(name=extraction_kit)
equipment = Equipment.query(asset_number=self.asset_number)
@@ -335,15 +335,15 @@ class PydEquipment(BaseModel, extra='ignore'):
logger.error("No equipment found. Returning None.")
return
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:
assoc = SubmissionEquipmentAssociation.query(equipment_id=equipment.id, submission_id=submission.id,
role=self.role, limit=1)
assoc = RunEquipmentAssociation.query(equipment_id=equipment.id, submission_id=submission.id,
role=self.role, limit=1)
except TypeError as e:
logger.error(f"Couldn't get association due to {e}, returning...")
assoc = 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?
# 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.
@@ -360,7 +360,7 @@ class PydEquipment(BaseModel, extra='ignore'):
logger.warning(f"Found already existing association: {assoc}")
assoc = None
else:
logger.warning(f"No submission found")
logger.warning(f"No run found")
assoc = None
return equipment, assoc, report
@@ -519,7 +519,7 @@ class PydSubmission(BaseModel, extra='allow'):
value['value'] = value['value'].strip()
return value
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"
else:
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):
super().__init__(**data)
# 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'])
self.namer = RSLNamer(self.rsl_plate_num['value'], submission_type=self.submission_type['value'])
if run_custom:
@@ -771,18 +771,18 @@ class PydSubmission(BaseModel, extra='allow'):
return missing_info, missing_reagents
@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:
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object
Tuple[BasicRun, Result]: BasicRun instance, result object
"""
report = Report()
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)}")
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'])
# logger.debug(f"Created or queried instance: {instance}")
if instance is None:
@@ -808,7 +808,7 @@ class PydSubmission(BaseModel, extra='allow'):
reagent = reagent.to_sql(submission=instance)
case "samples":
for sample in self.samples:
sample, associations, _ = sample.to_sql(submission=instance)
sample, associations, _ = sample.to_sql(run=instance)
for assoc in associations:
if assoc is not None:
if assoc not in instance.submission_sample_associations:
@@ -1186,7 +1186,7 @@ class PydEquipmentRole(BaseModel):
Args:
parent (_type_): parent widget
used (list): list of equipment already added to submission
used (list): list of equipment already added to run
Returns:
RoleComboBox: widget
@@ -1203,7 +1203,7 @@ class PydPCRControl(BaseModel):
ct: float
reagent_lot: str
submitted_date: datetime #: Date submitted to Robotics
submission_id: int
run_id: int
controltype_name: str
@report_result
@@ -1231,7 +1231,7 @@ class PydIridaControl(BaseModel, extra='ignore'):
kraken2_db_version: str
sample_id: int
submitted_date: datetime #: Date submitted to Robotics
submission_id: int
run_id: int
controltype_name: str
@field_validator("refseq_version", "kraken2_version", "kraken2_db_version", mode='before')

View File

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

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path
from markdown import markdown
from pandas import ExcelWriter
from backend import Reagent, BasicSample, Organization, KitType, BasicSubmission
from backend import Reagent, BasicSample, Organization, KitType, BasicRun
from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development
@@ -211,7 +211,7 @@ class App(QMainWindow):
dlg = DateTypePicker(self)
if dlg.exec():
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")
writer = ExcelWriter(filepath, "openpyxl")
df.to_excel(writer)
@@ -239,7 +239,7 @@ class AddSubForm(QWidget):
self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report")
self.tabs.addTab(self.tab5, "Turnaround Times")
# NOTE: Create submission adder form
# NOTE: Create run adder form
self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout)

View File

@@ -6,7 +6,7 @@ from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (
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
import logging
from typing import Generator
@@ -16,7 +16,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, submission: BasicSubmission):
def __init__(self, parent, submission: BasicRun):
super().__init__(parent)
self.submission = submission
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
@@ -137,7 +137,7 @@ class RoleComboBox(QWidget):
if process.tip_roles:
for iii, tip_role in enumerate(process.tip_roles):
widget = QComboBox()
tip_choices = [item.name for item in tip_role.instances]
tip_choices = [item.name for item in tip_role.controls]
widget.setEditable(False)
widget.addItems(tip_choices)
widget.setObjectName(f"tips_{tip_role.name}")

View File

@@ -12,7 +12,7 @@ import logging, numpy as np
from pprint import pformat
from typing import Tuple, List
from pathlib import Path
from backend.db.models import BasicSubmission
from backend.db.models import BasicRun
logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,7 +20,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
# Main window class
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)
# NOTE: setting title
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,
QDialogButtonBox, QTextEdit, QGridLayout)
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
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 .functions import select_save_file, save_pdf
from pathlib import Path
@@ -23,10 +23,10 @@ logger = logging.getLogger(f"submissions.{__name__}")
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)
self.app = get_application_from_parent(parent)
@@ -51,8 +51,8 @@ class SubmissionDetails(QDialog):
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
match sub:
case BasicSubmission():
self.submission_details(submission=sub)
case BasicRun():
self.run_details(run=sub)
self.rsl_plate_num = sub.rsl_plate_num
case BasicSample():
self.sample_details(sample=sub)
@@ -203,52 +203,52 @@ class SubmissionDetails(QDialog):
logger.error(f"Reagent with lot {old_lot} not found.")
@pyqtSlot(str)
def submission_details(self, submission: str | BasicSubmission):
def run_details(self, run: str | BasicRun):
"""
Sets details view to summary of Submission.
Args:
submission (str | BasicSubmission): Submission of interest.
run (str | BasicRun): Submission of interest.
"""
logger.debug(f"Submission details.")
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
self.rsl_plate_num = submission.rsl_plate_num
self.base_dict = submission.to_dict(full_data=True)
if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run)
self.rsl_plate_num = run.rsl_plate_num
self.base_dict = run.to_dict(full_data=True)
# NOTE: don't want id
self.base_dict['platemap'] = submission.make_plate_map(sample_list=submission.hitpicked)
self.base_dict['excluded'] = submission.get_default_info("details_ignore")
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict)
self.base_dict['platemap'] = run.make_plate_map(sample_list=run.hitpicked)
self.base_dict['excluded'] = run.get_default_info("details_ignore")
self.base_dict, self.template = run.get_details_template(base_dict=self.base_dict)
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
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.webview.setHtml(self.html)
@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:
submission (str | BasicSubmission): Submission to be completed
run (str | BasicRun): Submission to be completed
Returns:
None
"""
logger.info(f"Signing off on {submission} - ({getuser()})")
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
submission.signed_by = getuser()
submission.completed_date = datetime.now()
submission.completed_date.replace(tzinfo=timezone)
submission.save()
self.submission_details(submission=self.rsl_plate_num)
logger.info(f"Signing off on {run} - ({getuser()})")
if isinstance(run, str):
run = BasicRun.query(rsl_plate_num=run)
run.signed_by = getuser()
run.completed_date = datetime.now()
run.completed_date.replace(tzinfo=timezone)
run.save()
self.run_details(run=self.rsl_plate_num)
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")
save_pdf(obj=self.webview, filename=fname)
@@ -256,10 +256,10 @@ class SubmissionDetails(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)
self.app = get_application_from_parent(parent)
@@ -282,7 +282,7 @@ class SubmissionComment(QDialog):
def parse_form(self) -> List[dict]:
"""
Adds comment to submission object.
Adds comment to run object.
"""
commenter = getuser()
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 sys
@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, Q
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
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 .functions import select_open_file
@@ -63,7 +63,7 @@ class pandasModel(QAbstractTableModel):
class SubmissionsSheet(QTableView):
"""
presents submission summary to user in tab1
presents run summary to user in tab1
"""
def __init__(self, parent) -> None:
@@ -74,20 +74,20 @@ class SubmissionsSheet(QTableView):
page_size = self.app.page_size
except AttributeError:
page_size = 250
self.setData(page=1, page_size=page_size)
self.set_data(page=1, page_size=page_size)
self.resizeColumnsToContents()
self.resizeRowsToContents()
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?
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
"""
# 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:
self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4)
@@ -108,7 +108,7 @@ class SubmissionsSheet(QTableView):
id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0)
id = id.sibling(id.row(), 0).data()
submission = BasicSubmission.query(id=id)
submission = BasicRun.query(id=id)
self.menu = QMenu(self)
self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys():
@@ -167,8 +167,8 @@ class SubmissionsSheet(QTableView):
for ii in range(6, len(run)):
new_run[f"column{str(ii - 5)}_vol"] = run[ii]
# NOTE: Lookup imported submissions
sub = BasicSubmission.query(rsl_plate_num=new_run['rsl_plate_num'])
# NOTE: If no such submission exists, move onto the next run
sub = BasicRun.query(rsl_plate_num=new_run['rsl_plate_num'])
# NOTE: If no such run exists, move onto the next run
if sub is None:
continue
try:
@@ -192,7 +192,7 @@ class SubmissionsSheet(QTableView):
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:
obj (QMainWindow): original app window
@@ -215,9 +215,9 @@ class SubmissionsSheet(QTableView):
experiment_name=run[4].strip(),
end_time=run[5].strip()
)
# NOTE: lookup imported submission
sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num'])
# NOTE: if imported submission doesn't exist move on to next run
# NOTE: lookup imported run
sub = BasicRun.query(rsl_number=new_run['rsl_plate_num'])
# NOTE: if imported run doesn't exist move on to next run
if sub is None:
continue
sub.set_attribute('pcr_info', new_run)
@@ -302,7 +302,7 @@ class SubmissionsTree(QTreeView):
id = int(id.data())
except ValueError:
return
BasicSubmission.query(id=id).show_details(self)
BasicRun.query(id=id).show_details(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 (
QWidget, QPushButton, QVBoxLayout,
@@ -10,11 +10,11 @@ from .functions import select_open_file, select_save_file
import logging
from pathlib import Path
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.db import (
Organization, SubmissionType, Reagent,
ReagentRole, KitTypeReagentRoleAssociation, BasicSubmission
ReagentRole, KitTypeReagentRoleAssociation, BasicRun
)
from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop
@@ -93,7 +93,7 @@ class SubmissionFormContainer(QWidget):
@report_result
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:
obj (QMainWindow): original app window
@@ -122,12 +122,12 @@ class SubmissionFormContainer(QWidget):
# NOTE: create sheetparser using excel sheet and context from gui
try:
# self.prsr = SheetParser(filepath=fname)
self.parser = InfoParserV2(filepath=fname)
self.parser = InfoParser(filepath=fname)
except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}")
return
except AttributeError:
self.parser = InfoParserV2(filepath=fname)
self.parser = InfoParser(filepath=fname)
self.pyd = self.parser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pyd.samples)}")
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.
Returns:
models.Reagent: the constructed reagent object to add to submission
models.Reagent: the constructed reagent object to add to run
"""
report = Report()
if not instance:
@@ -178,7 +178,7 @@ class SubmissionFormWidget(QWidget):
self.missing_info = []
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
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'])
self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore']
@@ -202,7 +202,7 @@ class SubmissionFormWidget(QWidget):
value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}")
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:
self.layout.addWidget(add_widget)
if k in self.__class__.update_reagent_fields:
@@ -223,7 +223,7 @@ class SubmissionFormWidget(QWidget):
reagent.flip_check(self.disabler.checkbox.isChecked())
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":
"""
Make an InfoItem widget to hold a field
@@ -248,7 +248,7 @@ class SubmissionFormWidget(QWidget):
widget = None
case _:
widget = self.InfoItem(parent=self, key=key, value=value, submission_type=submission_type,
sub_obj=sub_obj)
run_object=run_object)
if disable:
widget.input.setEnabled(False)
widget.input.setToolTip("Widget disabled to protect database integrity.")
@@ -373,14 +373,14 @@ class SubmissionFormWidget(QWidget):
return report
case _:
pass
# NOTE: add reagents to submission object
# NOTE: add reagents to run object
if base_submission is None:
return
for reagent in base_submission.reagents:
reagent.update_last_used(kit=base_submission.extraction_kit)
save_output = base_submission.save()
# NOTE: update summary sheet
self.app.table_widget.sub_wid.setData()
self.app.table_widget.sub_wid.set_data()
# NOTE: reset form
try:
check = save_output.results == []
@@ -393,7 +393,7 @@ class SubmissionFormWidget(QWidget):
def export_csv_function(self, fname: Path | None = None):
"""
Save the submission's csv file.
Save the run's csv file.
Args:
fname (Path | None, optional): Input filename. Defaults to None.
@@ -405,7 +405,7 @@ class SubmissionFormWidget(QWidget):
except PermissionError:
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
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:
"""
@@ -446,14 +446,14 @@ class SubmissionFormWidget(QWidget):
class InfoItem(QWidget):
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)
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value)
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)
try:
self.missing: bool = value['missing']
@@ -492,7 +492,7 @@ class SubmissionFormWidget(QWidget):
def set_widget(self, parent: QWidget, key: str, value: dict,
submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> QWidget:
sub_obj: BasicRun | None = None) -> QWidget:
"""
Creates form widget
@@ -568,7 +568,7 @@ class SubmissionFormWidget(QWidget):
except ValueError:
categories.insert(0, categories.pop(categories.index(submission_type)))
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 _:
if key in sub_obj.timestamps:
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
@@ -692,7 +692,7 @@ class SubmissionFormWidget(QWidget):
wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
return wanted_reagent, report
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
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.
@@ -801,7 +801,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
Report: Report on status of parse.
"""
report = Report()
logger.info(f"Hello from client submission form parser!")
logger.info(f"Hello from client run form parser!")
info = {}
reagents = []
for widget in self.findChildren(QWidget):

View File

@@ -43,7 +43,7 @@ class Summary(InfoPane):
orgs = self.org_select.get_checked()
self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
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_excel_button.setEnabled(True)
else: