From 75c665ea05af0d375c73fcd6898f26a137c94339 Mon Sep 17 00:00:00 2001 From: lwark Date: Tue, 13 May 2025 13:20:01 -0500 Subject: [PATCH] Overhauling database. --- src/scripts/import_irida.py | 4 +- src/submissions/backend/db/models/__init__.py | 34 +- src/submissions/backend/db/models/controls.py | 48 +- src/submissions/backend/db/models/kits.py | 232 ++- .../backend/db/models/submissions.py | 1741 ++++------------- src/submissions/backend/excel/__init__.py | 1 + src/submissions/backend/excel/parser.py | 76 +- src/submissions/backend/excel/reports.py | 22 +- src/submissions/backend/excel/writer.py | 28 +- .../backend/validators/__init__.py | 32 +- .../backend/validators/omni_gui_objects.py | 6 +- src/submissions/backend/validators/pydant.py | 68 +- .../visualizations/concentrations_chart.py | 10 +- src/submissions/frontend/widgets/app.py | 6 +- .../frontend/widgets/equipment_usage.py | 6 +- .../frontend/widgets/gel_checker.py | 4 +- .../frontend/widgets/submission_details.py | 62 +- .../frontend/widgets/submission_table.py | 32 +- .../frontend/widgets/submission_widget.py | 42 +- src/submissions/frontend/widgets/summary.py | 2 +- ...ion_details.html => basicrun_details.html} | 0 21 files changed, 729 insertions(+), 1727 deletions(-) rename src/submissions/templates/{basicsubmission_details.html => basicrun_details.html} (100%) diff --git a/src/scripts/import_irida.py b/src/scripts/import_irida.py index 41eb48e..3e51c4e 100644 --- a/src/scripts/import_irida.py +++ b/src/scripts/import_irida.py @@ -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() diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index aba2fee..ce3187c 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -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) diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 6bae5ef..c043150 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -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 diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 5702aba..fb9fae0 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -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"" + return f"" except AttributeError: - logger.error(f"Reagent {self.reagent.lot} submission association {self.reagent_id} has no submissions!") - return f"" + logger.error(f"Reagent {self.reagent.lot} run association {self.reagent_id} has no submissions!") + return f"" 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" | 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"" + return f"" - 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): diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index f5ab539..71e2b4d 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1,5 +1,5 @@ """ -Models for the main submission and sample types. +Models for the main run and sample types. """ from __future__ import annotations @@ -15,8 +15,7 @@ from operator import itemgetter from pprint import pformat from pandas import DataFrame from sqlalchemy.ext.hybrid import hybrid_property -from . import Base, BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, \ - SubmissionReagentAssociation, LogMixin +from . import Base, BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm.attributes import flag_modified @@ -35,34 +34,34 @@ from jinja2.exceptions import TemplateNotFound from jinja2 import Template from PIL import Image -from . import kittypes_submissions +from . import kittypes_runs logger = logging.getLogger(f"submissions.{__name__}") class ClientSubmission(BaseClass, LogMixin): """ - Object for the client submission from which all run objects will be created. + Object for the client run from which all run objects will be created. """ id = Column(INTEGER, primary_key=True) #: primary key - submitter_plate_id = Column(String(127), unique=True) #: The number given to the submission by the submitting lab - submitted_date = Column(TIMESTAMP) #: Date submission received + submitter_plate_id = Column(String(127), unique=True) #: The number given to the run by the submitting lab + submitted_date = Column(TIMESTAMP) #: Date run received submitting_lab = relationship("Organization", back_populates="submissions") #: client org submitting_lab_id = Column(INTEGER, ForeignKey("_organization.id", ondelete="SET NULL", name="fk_BS_sublab_id")) #: client lab id from _organizations _submission_category = Column( String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name - sample_count = Column(INTEGER) #: Number of samples in the submission + sample_count = Column(INTEGER) #: Number of samples in the run comment = Column(JSON) - runs = relationship("BasicSubmission", back_populates="client_submission") #: many-to-one relationship - misc_info = Column(JSON) + runs = relationship("BasicRun", back_populates="client_submission") #: many-to-one relationship + # misc_info = Column(JSON) contact = relationship("Contact", back_populates="submissions") #: client org contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL", name="fk_BS_contact_id")) #: client lab id from _organizations submission_type_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL", - name="fk_BS_subtype_name")) #: name of joined submission type - submission_type = relationship("SubmissionType", back_populates="instances") #: archetype of this submission + name="fk_BS_subtype_name")) #: name of joined run type + submission_type = relationship("SubmissionType", back_populates="controls") #: archetype of this run cost_centre = Column( @@ -70,7 +69,7 @@ class ClientSubmission(BaseClass, LogMixin): submission_sample_associations = relationship( "SubmissionSampleAssociation", - back_populates="submission", + back_populates="run", cascade="all, delete-orphan", ) #: Relation to SubmissionSampleAssociation @@ -120,7 +119,7 @@ class ClientSubmission(BaseClass, LogMixin): page: int = 1, page_size: None | int = 250, **kwargs - ) -> BasicSubmission | List[BasicSubmission]: + ) -> BasicRun | List[BasicRun]: """ Lookup submissions based on a number of parameters. Overrides parent. @@ -130,14 +129,14 @@ class ClientSubmission(BaseClass, LogMixin): rsl_plate_num (str | None, optional): Submission name in the database (limits results to 1). Defaults to None. start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None. end_date (date | str | int | None, optional): Ending date to search by. Defaults to None. - reagent (models.Reagent | str | None, optional): A reagent used in the submission. Defaults to None. + reagent (models.Reagent | str | None, optional): A reagent used in the run. Defaults to None. chronologic (bool, optional): Return results in chronologic order. Defaults to False. limit (int, optional): Maximum number of results to return. Defaults to 0. Returns: - models.BasicSubmission | List[models.BasicSubmission]: Submission(s) of interest + models.BasicRun | List[models.BasicRun]: Submission(s) of interest """ - # from ... import SubmissionReagentAssociation + # from ... import RunReagentAssociation # NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters query: Query = cls.__database_session__.query(cls) if start_date is not None and end_date is None: @@ -146,7 +145,7 @@ class ClientSubmission(BaseClass, LogMixin): if end_date is not None and start_date is None: # NOTE: this query returns a tuple of (object, datetime), need to get only datetime. start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1] - logger.warning(f"End date with no start date, using first submission date: {start_date}") + logger.warning(f"End date with no start date, using first run date: {start_date}") if start_date is not None: start_date = cls.rectify_query_date(start_date) end_date = cls.rectify_query_date(end_date, eod=True) @@ -290,9 +289,9 @@ class ClientSubmission(BaseClass, LogMixin): return output -class BasicSubmission(BaseClass, LogMixin): +class BasicRun(BaseClass, LogMixin): """ - Object for an entire submission run. Links to client submissions, reagents, equipment, processes + Object for an entire run run. Links to client submissions, reagents, equipment, processes """ @@ -302,75 +301,28 @@ class BasicSubmission(BaseClass, LogMixin): client_submission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL", name="fk_BS_clientsub_id")) #: client lab id from _organizations) client_submission = relationship("ClientSubmission", back_populates="runs") - # submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab started_date = Column(TIMESTAMP) #: Date this run was started. - # submitting_lab = relationship("Organization", back_populates="submissions") #: client org - # submitting_lab_id = Column(INTEGER, ForeignKey("_organization.id", ondelete="SET NULL", - # name="fk_BS_sublab_id")) #: client lab id from _organizations - #sample_count = Column(INTEGER) #: Number of samples in the submission - # kittypes = relationship("KitType", back_populates="runs") #: The extraction kit used - # kittype_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete="SET NULL", - # name="fk_BS_extkit_id")) #: id of joined extraction kit - # submission_type_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL", - # name="fk_BS_subtype_name")) #: name of joined submission type - technician = Column(JSON) #: name of processing tech(s) - # reagents_id = Column(String, ForeignKey("_reagent.id", ondelete="SET NULL", - # name="fk_BS_reagents_id")) #: id of used reagents - misc_info = Column(JSON) #: unstructured output for overflow info. + run_cost = Column( FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation. - signed_by = Column(String(32)) #: user name of person who submitted the submission to the database. + signed_by = Column(String(32)) #: user name of person who submitted the run to the database. comment = Column(JSON) #: user notes - # submission_category = Column( - # String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name - # cost_centre = Column( - # String(64)) #: Permanent storage of used cost centre in case organization field changed in the future. - # contact = relationship("Contact", back_populates="submissions") #: client org - # contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL", - # name="fk_BS_contact_id")) #: client lab id from _organizations custom = Column(JSON) - controls = relationship("Control", back_populates="submission", - uselist=True) #: A control sample added to submission + completed_date = Column(TIMESTAMP) - kittypes = relationship("KitType", back_populates="submissions", - secondary=kittypes_submissions) #: submissions this kit was used for + procedures = relationship("Procedure", back_populates="run", uselist=True) - submission_sample_associations = relationship( - "SubmissionSampleAssociation", - back_populates="submission", + run_sample_associations = relationship( + "RunSampleAssociation", + back_populates="run", cascade="all, delete-orphan", ) #: Relation to SubmissionSampleAssociation - samples = association_proxy("submission_sample_associations", - "sample", creator=lambda sample: SubmissionSampleAssociation( + samples = association_proxy("run_sample_associations", + "sample", creator=lambda sample: RunSampleAssociation( sample=sample)) #: Association proxy to SubmissionSampleAssociation.samples - submission_reagent_associations = relationship( - "SubmissionReagentAssociation", - back_populates="submission", - cascade="all, delete-orphan", - ) #: Relation to SubmissionReagentAssociation - - reagents = association_proxy("submission_reagent_associations", - "reagent") #: Association proxy to SubmissionReagentAssociation.reagent - - submission_equipment_associations = relationship( - "SubmissionEquipmentAssociation", - back_populates="submission", - cascade="all, delete-orphan" - ) #: Relation to Equipment - - equipment = association_proxy("submission_equipment_associations", - "equipment") #: Association proxy to SubmissionEquipmentAssociation.equipment - - submission_tips_associations = relationship( - "SubmissionTipsAssociation", - back_populates="submission", - cascade="all, delete-orphan") - - tips = association_proxy("submission_tips_associations", - "tips") # NOTE: Allows for subclassing into ex. BacterialCulture, Wastewater, etc. # __mapper_args__ = { @@ -401,40 +353,14 @@ class BasicSubmission(BaseClass, LogMixin): def name(self): return self.rsl_plate_num - @classproperty - def jsons(cls) -> List[str]: - """ - Get list of JSON db columns - - Returns: - List[str]: List of column names - """ - output = [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)] - if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission": - output += BasicSubmission.jsons - return output - - @classproperty - def timestamps(cls) -> List[str]: - """ - Get list of TIMESTAMP columns - - Returns: - List[str]: List of column names - """ - output = [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)] - if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission": - output += BasicSubmission.timestamps - return output - @classmethod def get_default_info(cls, *args, submission_type: SubmissionType | None = None) -> dict: """ - Gets default info from the database for a given submission type. + Gets default info from the database for a given run type. Args: *args (): List of fields to get - submission_type (SubmissionType): the submission type of interest. Necessary due to generic submission types. + submission_type (SubmissionType): the run type of interest. Necessary due to generic run types. Returns: dict: Default info @@ -465,7 +391,7 @@ class BasicSubmission(BaseClass, LogMixin): else: st = cls.get_submission_type(submission_type) if st is None: - logger.error("No default info for BasicSubmission.") + logger.error("No default info for BasicRun.") else: output['submission_type'] = st.name for k, v in st.defaults.items(): @@ -493,7 +419,7 @@ class BasicSubmission(BaseClass, LogMixin): Gets the SubmissionType associated with this class Args: - sub_type (str | SubmissionType, Optional): Identity of the submission type to retrieve. Defaults to None. + sub_type (str | SubmissionType, Optional): Identity of the run type to retrieve. Defaults to None. Returns: SubmissionType: SubmissionType with name equal sub_type or this polymorphic identity if sub_type is None. @@ -517,7 +443,7 @@ class BasicSubmission(BaseClass, LogMixin): def construct_info_map(cls, submission_type: SubmissionType | None = None, mode: Literal["read", "write"] = "read") -> dict: """ - Method to call submission type's construct info map. + Method to call run type's construct info map. Args: mode (Literal["read", "write"]): Which map to construct. @@ -530,7 +456,7 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod def construct_sample_map(cls, submission_type: SubmissionType | None = None) -> dict: """ - Method to call submission type's construct_sample_map + Method to call run type's construct_sample_map Returns: dict: sample location map @@ -670,7 +596,7 @@ class BasicSubmission(BaseClass, LogMixin): query_out = [] for sub_type in submissiontype: subs = cls.query(page_size=0, start_date=start_date, end_date=end_date, submissiontype=sub_type) - # logger.debug(f"Sub results: {subs}") + # logger.debug(f"Sub results: {runs}") query_out.append(subs) query_out = list(itertools.chain.from_iterable(query_out)) else: @@ -690,7 +616,7 @@ class BasicSubmission(BaseClass, LogMixin): @property def column_count(self) -> int: """ - Calculate the number of columns in this submission + Calculate the number of columns in this run Returns: int: Number of unique columns. @@ -707,8 +633,8 @@ class BasicSubmission(BaseClass, LogMixin): cols_count_96 = self.column_count except Exception as e: logger.error(f"Column count error: {e}") - # NOTE: Get kit associated with this submission - # logger.debug(f"Checking associations with submission type: {self.submission_type_name}") + # NOTE: Get kit associated with this run + # logger.debug(f"Checking associations with run type: {self.submission_type_name}") assoc = next((item for item in self.extraction_kit.kit_submissiontype_associations if item.submission_type == self.submission_type), None) @@ -741,10 +667,10 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod def make_plate_map(cls, sample_list: list, plate_rows: int = 8, plate_columns=12) -> str: """ - Constructs an html based plate map for submission details. + Constructs an html based plate map for run details. Args: - sample_list (list): List of submission samples + sample_list (list): List of run samples plate_rows (int, optional): Number of rows in the plate. Defaults to 8. plate_columns (int, optional): Number of columns in the plate. Defaults to 12. @@ -768,7 +694,7 @@ class BasicSubmission(BaseClass, LogMixin): @property def used_equipment(self) -> Generator[str, None, None]: """ - Gets EquipmentRole names associated with this BasicSubmission + Gets EquipmentRole names associated with this BasicRun Returns: List[str]: List of names @@ -830,7 +756,7 @@ class BasicSubmission(BaseClass, LogMixin): field_value = Contact.query(name=value) case "samples": for sample in value: - sample, _ = sample.to_sql(submission=self) + sample, _ = sample.to_sql(run=self) return case "reagents": field_value = [reagent['value'].to_sql()[0] if isinstance(reagent, dict) else reagent.to_sql()[0] for @@ -886,7 +812,7 @@ class BasicSubmission(BaseClass, LogMixin): def update_subsampassoc(self, assoc: SubmissionSampleAssociation, input_dict: dict) -> SubmissionSampleAssociation: """ - Update a joined submission sample association. + Update a joined run sample association. Args: assoc (SubmissionSampleAssociation): Sample association to be updated. @@ -912,7 +838,7 @@ class BasicSubmission(BaseClass, LogMixin): assoc.reagent = reagent except StopIteration as e: logger.error(f"Association for {role} not found, creating new association.") - assoc = SubmissionReagentAssociation(submission=self, reagent=reagent) + assoc = RunReagentAssociation(submission=self, reagent=reagent) self.submission_reagent_associations.append(assoc) def to_pydantic(self, backup: bool = False) -> "PydSubmission": @@ -982,10 +908,10 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod def get_regex(cls, submission_type: SubmissionType | str | None = None) -> re.Pattern: """ - Gets the regex string for identifying a certain class of submission. + Gets the regex string for identifying a certain class of run. Args: - submission_type (SubmissionType | str | None, optional): submission type of interest. Defaults to None. + submission_type (SubmissionType | str | None, optional): run type of interest. Defaults to None. Returns: str: String from which regex will be compiled. @@ -994,7 +920,7 @@ class BasicSubmission(BaseClass, LogMixin): try: regex = cls.get_submission_type(submission_type).defaults['regex'] except AttributeError as e: - logger.error(f"Couldn't get submission type for {cls.__mapper_args__['polymorphic_identity']}") + logger.error(f"Couldn't get run type for {cls.__mapper_args__['polymorphic_identity']}") regex = None try: regex = re.compile(rf"{regex}", flags=re.IGNORECASE | re.VERBOSE) @@ -1011,7 +937,7 @@ class BasicSubmission(BaseClass, LogMixin): Constructs catchall regex. Returns: - re.Pattern: Regular expression pattern to discriminate between submission types. + re.Pattern: Regular expression pattern to discriminate between run types. """ res = [st.defaults['regex'] for st in SubmissionType.query() if st.defaults] rstring = rf'{"|".join(res)}' @@ -1020,7 +946,7 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod def find_polymorphic_subclass(cls, polymorphic_identity: str | SubmissionType | list | None = None, - attrs: dict | None = None) -> BasicSubmission | List[BasicSubmission]: + attrs: dict | None = None) -> BasicRun | List[BasicRun]: """ Find subclass based on polymorphic identity or relevant attributes. @@ -1042,7 +968,7 @@ class BasicSubmission(BaseClass, LogMixin): 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 list(): output = [] for identity in polymorphic_identity: @@ -1067,7 +993,7 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None, custom_fields: dict = {}) -> dict: """ - Update submission dictionary with type specific information + Update run dictionary with type specific information Args: input_dict (dict): Input sample dictionary @@ -1119,7 +1045,7 @@ class BasicSubmission(BaseClass, LogMixin): Args: input_dict (dict): Parser product up to this point. - xl (pd.ExcelFile | None, optional): Excel submission form. Defaults to None. + xl (pd.ExcelFile | None, optional): Excel run form. Defaults to None. info_map (dict | None, optional): Map of information locations from SubmissionType. Defaults to None. plate_map (dict | None, optional): Constructed plate map of samples. Defaults to None. @@ -1132,7 +1058,7 @@ class BasicSubmission(BaseClass, LogMixin): def custom_info_writer(cls, input_excel: Workbook, info: dict | None = None, backup: bool = False, custom_fields: dict = {}) -> Workbook: """ - Adds custom autofill methods for submission + Adds custom autofill methods for run Args: input_excel (Workbook): initial workbook. @@ -1169,7 +1095,7 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod def custom_sample_writer(self, sample: dict) -> dict: """ - Performs any final alterations to sample writing unique to this submission type. + Performs any final alterations to sample writing unique to this run type. Args: sample (dict): Dictionary of sample values. @@ -1262,7 +1188,7 @@ class BasicSubmission(BaseClass, LogMixin): Args: xl (Workbook): D&A export file - rsl_plate_num (str): Plate number of the submission to be joined. + rsl_plate_num (str): Plate number of the run to be joined. Yields: Generator[dict, None, None]: Dictionaries of row values. @@ -1357,7 +1283,7 @@ class BasicSubmission(BaseClass, LogMixin): template = env.get_template(temp_name) except TemplateNotFound as e: logger.error(f"Couldn't find template due to {e}") - template = env.get_template("basicsubmission_details.html") + template = env.get_template("basicrun_details.html") return base_dict, template # NOTE: Query functions @@ -1377,7 +1303,7 @@ class BasicSubmission(BaseClass, LogMixin): page: int = 1, page_size: None | int = 250, **kwargs - ) -> BasicSubmission | List[BasicSubmission]: + ) -> BasicRun | List[BasicRun]: """ Lookup submissions based on a number of parameters. Overrides parent. @@ -1387,14 +1313,14 @@ class BasicSubmission(BaseClass, LogMixin): rsl_plate_num (str | None, optional): Submission name in the database (limits results to 1). Defaults to None. start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None. end_date (date | str | int | None, optional): Ending date to search by. Defaults to None. - reagent (models.Reagent | str | None, optional): A reagent used in the submission. Defaults to None. + reagent (models.Reagent | str | None, optional): A reagent used in the run. Defaults to None. chronologic (bool, optional): Return results in chronologic order. Defaults to False. limit (int, optional): Maximum number of results to return. Defaults to 0. Returns: - models.BasicSubmission | List[models.BasicSubmission]: Submission(s) of interest + models.BasicRun | List[models.BasicRun]: Run(s) of interest """ - # from ... import SubmissionReagentAssociation + # from ... import RunReagentAssociation # NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters if submissiontype is not None: model = cls.find_polymorphic_subclass(polymorphic_identity=submissiontype) @@ -1410,7 +1336,7 @@ class BasicSubmission(BaseClass, LogMixin): if end_date is not None and start_date is None: # NOTE: this query returns a tuple of (object, datetime), need to get only datetime. start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1] - logger.warning(f"End date with no start date, using first submission date: {start_date}") + logger.warning(f"End date with no start date, using first run date: {start_date}") if start_date is not None: # match start_date: # case date(): @@ -1448,11 +1374,11 @@ class BasicSubmission(BaseClass, LogMixin): # NOTE: by reagent (for some reason) match reagent: case str(): - query = query.join(SubmissionReagentAssociation).join(Reagent).filter( + query = query.join(RunReagentAssociation).join(Reagent).filter( Reagent.lot == reagent) case Reagent(): - query = query.join(SubmissionReagentAssociation).filter( - SubmissionReagentAssociation.reagent == reagent) + query = query.join(RunReagentAssociation).filter( + RunReagentAssociation.reagent == reagent) case _: pass # NOTE: by rsl number (returns only a single value) @@ -1489,7 +1415,7 @@ class BasicSubmission(BaseClass, LogMixin): return cls.execute_query(query=query, model=model, limit=limit, **kwargs) @classmethod - def query_or_create(cls, submission_type: str | SubmissionType | None = None, **kwargs) -> BasicSubmission: + def query_or_create(cls, submission_type: str | SubmissionType | None = None, **kwargs) -> BasicRun: """ Returns object from db if exists, else, creates new. Due to need for user input, doesn't see much use ATM. @@ -1501,7 +1427,7 @@ class BasicSubmission(BaseClass, LogMixin): ValueError: Raised if disallowed key is passed. Returns: - cls: A BasicSubmission subclass. + cls: A BasicRun subclass instance. """ code = 0 msg = "" @@ -1527,15 +1453,15 @@ class BasicSubmission(BaseClass, LogMixin): from frontend.widgets.pop_ups import QuestionAsker logger.warning(f"Found existing instance: {instance}, asking to overwrite.") # code = 1 - # msg = "This submission already exists.\nWould you like to overwrite?" + # msg = "This run already exists.\nWould you like to overwrite?" # report.add_result(Result(msg=msg, code=code)) dlg = QuestionAsker(title="Overwrite?", - message="This submission already exists.\nWould you like to overwrite?") + message="This run already exists.\nWould you like to overwrite?") if dlg.exec(): pass else: code = 1 - msg = "This submission already exists.\nWould you like to overwrite?" + msg = "This run already exists.\nWould you like to overwrite?" report.add_result(Result(msg=msg, code=code)) return None, report return instance, report @@ -1580,13 +1506,13 @@ class BasicSubmission(BaseClass, LogMixin): self.__database_session__.rollback() raise e try: - obj.setData() + obj.set_data() except AttributeError: logger.error("App will not refresh data at this time.") def show_details(self, obj): """ - Creates Widget for showing submission details. + Creates Widget for showing run details. Args: obj (Widget): Parent widget @@ -1598,7 +1524,7 @@ class BasicSubmission(BaseClass, LogMixin): def edit(self, obj): """ - Return submission to form widget for updating + Return run to form widget for updating Args: obj (Widget): Parent widget @@ -1628,7 +1554,7 @@ class BasicSubmission(BaseClass, LogMixin): def add_equipment(self, obj): """ - Creates widget for adding equipment to this submission + Creates widget for adding equipment to this run Args: obj (_type_): parent widget @@ -1653,7 +1579,7 @@ class BasicSubmission(BaseClass, LogMixin): if tassoc not in self.submission_tips_associations: tassoc.save() else: - logger.error(f"Tips already found in submission, skipping.") + logger.error(f"Tips already found in run, skipping.") else: pass @@ -1704,959 +1630,6 @@ class BasicSubmission(BaseClass, LogMixin): return delta -# NOTE: Below are the custom submission types - -# class BacterialCulture(BasicSubmission): -# """ -# derivative submission type from BasicSubmission -# """ -# id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True) -# -# __mapper_args__ = dict(polymorphic_identity="Bacterial Culture", -# polymorphic_load="inline", -# inherit_condition=(id == BasicSubmission.id)) -# -# def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict: -# """ -# Extends parent class method to add controls to dict -# -# Returns: -# dict: dictionary used in submissions summary -# """ -# output = super().to_dict(full_data=full_data, backup=backup, report=report) -# if report: -# return output -# if full_data: -# output['controls'] = [item.to_sub_dict() for item in self.controls] -# return output -# -# @classmethod -# def filename_template(cls): -# """ -# extends parent -# """ -# template = super().filename_template() -# template += "_{{ submitting_lab.name }}_{{ submitter_plate_num }}" -# return template -# -# @classmethod -# def custom_validation(cls, pyd) -> "PydSubmission": -# """ -# Extends parent. Currently finds control sample and adds to reagents. -# -# Args: -# input_dict (dict): _description_ -# xl (pd.ExcelFile | None, optional): _description_. Defaults to None. -# info_map (dict | None, optional): _description_. Defaults to None. -# -# Returns: -# PydSubmission: Updated pydantic. -# """ -# from . import ControlType -# pyd = super().custom_validation(pyd) -# # NOTE: build regex for all control types that have targets -# regex = ControlType.build_positive_regex(control_type="Irida Control") -# # NOTE: search samples for match -# for sample in pyd.samples: -# matched = regex.match(sample.submitter_id) -# if bool(matched): -# new_lot = matched.group() -# try: -# pos_control_reg = \ -# next(reg for reg in pyd.reagents if reg.role == "Bacterial-Positive Control") -# except StopIteration: -# logger.error(f"No positive control reagent listed") -# return pyd -# pos_control_reg.lot = new_lot -# pos_control_reg.missing = False -# return pyd -# -# @classmethod -# def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None, custom_fields: dict = {}) -> dict: -# """ -# Performs class specific info parsing before info parsing is finalized. -# -# Args: -# input_dict (dict): Generic input info -# xl (Workbook | None, optional): Original xl workbook. Defaults to None. -# custom_fields (dict, optional): Map of custom fields to be parsed. Defaults to {}. -# -# Returns: -# dict: Updated info dictionary. -# """ -# input_dict = super().custom_info_parser(input_dict=input_dict, xl=xl, custom_fields=custom_fields) -# return input_dict -# -# def custom_context_events(self) -> dict: -# """ -# Sets context events for main widget -# -# Returns: -# dict: Context menu items for this instance. -# """ -# events = super().custom_context_events() -# events['Import Concentration'] = self.import_concentration -# return events -# -# @report_result -# def import_concentration(self, obj) -> Report: -# from frontend.widgets import select_open_file -# from backend.excel.parser import ConcentrationParser -# report = Report() -# fname = select_open_file(obj=obj, file_extension="xlsx") -# if not fname: -# report.add_result(Result(msg="No file selected, cancelling.", status="Warning")) -# return report -# parser = ConcentrationParser(filepath=fname, submission=self) -# conc_samples = [sample for sample in parser.samples] -# # logger.debug(f"Concentration samples: {pformat(conc_samples)}") -# for sample in self.samples: -# # logger.debug(f"Sample {sample.submitter_id}") -# # logger.debug(f"Match {item['submitter_id']}") -# try: -# # NOTE: Fix for ENs which have no rsl_number... -# sample_dict = next( -# item for item in conc_samples if str(item['submitter_id']).upper() == sample.submitter_id) -# except StopIteration: -# logger.error(f"Couldn't find sample dict for {sample.submitter_id}") -# continue -# logger.debug(f"Sample {sample.submitter_id} conc. = {sample_dict['concentration']}") -# if sample_dict['concentration']: -# sample.concentration = sample_dict['concentration'] -# else: -# continue -# sample.save() -# # logger.debug(conc_samples) -# return report -# -# @classmethod -# def parse_concentration(cls, xl: Workbook, rsl_plate_num: str) -> Generator[dict, None, None]: -# lookup_table = cls.get_submission_type().sample_map['lookup_table'] -# logger.debug(lookup_table) -# main_sheet = xl[lookup_table['sheet']] -# for row in main_sheet.iter_rows(min_row=lookup_table['start_row'], max_row=lookup_table['end_row']): -# idx = row[0].row -# sample = dict( -# submitter_id=main_sheet.cell(row=idx, column=lookup_table['sample_columns']['submitter_id']).value) -# sample['concentration'] = main_sheet.cell(row=idx, -# column=lookup_table['sample_columns']['concentration']).value -# yield sample -# -# # def get_provisional_controls(self, controls_only: bool = True): -# def get_provisional_controls(self, include: List[str] = []): -# # NOTE To ensure Samples are done last. -# include = sorted(include) -# # logger.debug(include) -# pos_str = "(ATCC)|(MCS)" -# pos_regex = re.compile(rf"^{pos_str}") -# neg_str = "(EN)" -# neg_regex = re.compile(rf"^{neg_str}") -# output = [] -# for item in include: -# match item: -# case "Positive": -# if self.controls: -# provs = (control.sample for control in self.controls if control.is_positive_control) -# else: -# provs = (sample for sample in self.samples if bool(pos_regex.match(sample.submitter_id))) -# case "Negative": -# if self.controls: -# provs = (control.sample for control in self.controls if not control.is_positive_control) -# else: -# provs = (sample for sample in self.samples if bool(neg_regex.match(sample.submitter_id))) -# case _: -# provs = (sample for sample in self.samples if not sample.control and sample not in output) -# for prov in provs: -# # logger.debug(f"Prov: {prov}") -# prov.submission = self.rsl_plate_num -# prov.submitted_date = self.submitted_date -# output.append(prov) -# return output -# -# -# class Wastewater(BasicSubmission): -# """ -# derivative submission type from BasicSubmission -# """ -# id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True, autoincrement=False) -# ext_technician = Column(String(64)) #: Name of technician doing extraction -# pcr_technician = Column(String(64)) #: Name of technician doing pcr -# pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic) -# -# __mapper_args__ = __mapper_args__ = dict(polymorphic_identity="Wastewater", -# polymorphic_load="inline", -# inherit_condition=(id == BasicSubmission.id)) -# -# def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict: -# """ -# Extends parent class method to add controls to dict -# -# Returns: -# dict: dictionary used in submissions summary -# """ -# output = super().to_dict(full_data=full_data, backup=backup, report=report) -# if report: -# return output -# try: -# output['pcr_info'] = self.pcr_info -# except TypeError as e: -# pass -# if self.ext_technician is None or self.ext_technician == "None": -# output['ext_technician'] = self.technician -# else: -# output["ext_technician"] = self.ext_technician -# if self.pcr_technician is None or self.pcr_technician == "None": -# output["pcr_technician"] = self.technician -# else: -# output['pcr_technician'] = self.pcr_technician -# if full_data: -# output['samples'] = [sample for sample in output['samples']] -# dummy_samples = [] -# for item in output['samples']: -# thing = deepcopy(item) -# try: -# thing['row'] = thing['source_row'] -# thing['column'] = thing['source_column'] -# except KeyError: -# logger.error(f"No row or column for sample: {item['submitter_id']}") -# continue -# thing['tooltip'] = f"Sample Name: {thing['name']}\nWell: {thing['sample_location']}" -# dummy_samples.append(thing) -# # logger.debug(f"Dummy samples for 24 well: {pformat(dummy_samples)}") -# output['origin_plate'] = self.__class__.make_plate_map(sample_list=dummy_samples, plate_rows=4, -# plate_columns=6) -# # logger.debug(f"PCR info: {output['pcr_info']}") -# return output -# -# @classmethod -# def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None, custom_fields: dict = {}) -> dict: -# """ -# Update submission dictionary with class specific information. Extends parent -# -# Args: -# input_dict (dict): Input sample dictionary -# xl (Workbook): xl (Workbook): original xl workbook, used for child classes mostly. -# custom_fields: Dictionary of locations, ranges, etc to be used by this function -# -# Returns: -# dict: Updated info dictionary -# """ -# input_dict = super().custom_info_parser(input_dict) -# if xl is not None: -# try: -# input_dict['csv'] = xl["Copy to import file"] -# except KeyError as e: -# logger.error(e) -# try: -# match input_dict['rsl_plate_num']: -# case dict(): -# input_dict['csv'] = xl[input_dict['rsl_plate_num']['value']] -# case str(): -# input_dict['csv'] = xl[input_dict['rsl_plate_num']] -# case _: -# pass -# except Exception as e: -# logger.error(f"Error handling couldn't get csv due to: {e}") -# return input_dict -# -# @classmethod -# def parse_samples(cls, input_dict: dict) -> dict: -# """ -# Update sample dictionary with type specific information. Extends parent -# -# Args: -# input_dict (dict): Input sample dictionary -# -# Returns: -# dict: Updated sample dictionary -# """ -# input_dict = super().parse_samples(input_dict=input_dict) -# # NOTE: Had to put in this section due to ENs not having rsl_number and therefore not getting PCR results. -# check = check_key_or_attr("rsl_number", input_dict) -# if not check: -# input_dict['rsl_number'] = input_dict['submitter_id'] -# # logger.debug(pformat(input_dict, indent=4)) -# return input_dict -# -# @classmethod -# def parse_pcr(cls, xl: Workbook, rsl_plate_num: str) -> Generator[dict, None, None]: -# """ -# Perform parsing of pcr info. Since most of our PC outputs are the same format, this should work for most. -# -# Args: -# xl (pd.DataFrame): pcr info form -# rsl_plate_number (str): rsl plate num of interest -# -# Returns: -# Generator[dict, None, None]: Updated samples -# """ -# samples = [item for item in super().parse_pcr(xl=xl, rsl_plate_num=rsl_plate_num)] -# # NOTE: Due to having to run through samples in for loop we need to convert to list. -# # NOTE: Also, you can't change the size of a list while iterating it, so don't even think about it. -# output = [] -# for sample in samples: -# logger.debug(sample) -# # NOTE: remove '-{target}' from controls -# sample['sample'] = re.sub('-N\\d*$', '', sample['sample']) -# # NOTE: if sample is already in output skip -# if sample['sample'] in [item['sample'] for item in output]: -# logger.warning(f"Already have {sample['sample']}") -# continue -# # NOTE: Set ct values -# # logger.debug(f"Sample ct: {sample['ct']}") -# sample[f"ct_{sample['target'].lower()}"] = sample['ct'] if isinstance(sample['ct'], float) else 0.0 -# # NOTE: Set assessment -# # logger.debug(f"Sample assessemnt: {sample['assessment']}") -# # NOTE: Get sample having other target -# other_targets = [s for s in samples if re.sub('-N\\d*$', '', s['sample']) == sample['sample']] -# for s in other_targets: -# sample[f"ct_{s['target'].lower()}"] = s['ct'] if isinstance(s['ct'], float) else 0.0 -# try: -# del sample['ct'] -# except KeyError: -# pass -# try: -# del sample['assessment'] -# except KeyError: -# pass -# # logger.debug(sample) -# row = int(row_keys[sample['well'][:1]]) -# column = int(sample['well'][1:]) -# sample['row'] = row -# sample['column'] = column -# output.append(sample) -# # NOTE: And then convert back to list to keep fidelity with parent method. -# for sample in output: -# yield sample -# -# @classmethod -# def enforce_name(cls, instr: str, data: dict | None = {}) -> str: -# """ -# Custom naming method for this class. Extends parent. -# -# Args: -# instr (str): Initial name. -# data (dict | None, optional): Additional parameters for name. Defaults to None. -# -# Returns: -# str: Updated name. -# """ -# try: -# # NOTE: Deal with PCR file. -# instr = re.sub(r"PCR(-|_)", "", instr) -# except (AttributeError, TypeError) as e: -# logger.error(f"Problem using regex: {e}") -# outstr = super().enforce_name(instr=instr, data=data) -# return outstr -# -# @classmethod -# def adjust_autofill_samples(cls, samples: List[Any]) -> List[Any]: -# """ -# Makes adjustments to samples before writing to excel. Extends parent. -# -# Args: -# samples (List[Any]): List of Samples -# -# Returns: -# List[Any]: Updated list of samples -# """ -# samples = super().adjust_autofill_samples(samples) -# samples = [item for item in samples if not item.submitter_id.startswith("EN")] -# return samples -# -# @classmethod -# def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]: -# """ -# Get the details jinja template for the correct class. Extends parent -# -# Args: -# base_dict (dict): incoming dictionary of Submission fields -# -# Returns: -# Tuple[dict, Template]: (Updated dictionary, Template to be rendered) -# """ -# base_dict, template = super().get_details_template(base_dict=base_dict) -# base_dict['excluded'] += ['origin_plate'] -# return base_dict, template -# -# def custom_context_events(self) -> dict: -# """ -# Sets context events for main widget -# -# Returns: -# dict: Context menu items for this instance. -# """ -# events = super().custom_context_events() -# events['Link PCR'] = self.link_pcr -# return events -# -# @report_result -# def link_pcr(self, obj) -> Report: -# """ -# PYQT6 function to add PCR info to this submission -# -# Args: -# obj (_type_): Parent widget -# """ -# from backend.excel import PCRParser -# from backend.db import PCRControl, ControlType -# from frontend.widgets import select_open_file -# report = Report() -# fname = select_open_file(obj=obj, file_extension="xlsx") -# if not fname: -# report.add_result(Result(msg="No file selected, cancelling.", status="Warning")) -# return report -# parser = PCRParser(filepath=fname, submission=self) -# self.set_attribute("pcr_info", parser.pcr_info) -# # NOTE: These are generators here, need to expand. -# pcr_samples = sorted([sample for sample in parser.samples], key=itemgetter('column')) -# logger.debug(f"Samples from parser: {pformat(pcr_samples)}") -# # NOTE: Samples from parser check out. -# pcr_controls = [control for control in parser.controls] -# self.save(original=False) -# for assoc in self.submission_sample_associations: -# logger.debug(f"Checking pcr_samples for {assoc.sample.rsl_number}, {assoc.sample.ww_full_sample_id} at " -# f"column {assoc.column} and row {assoc.row}") -# # NOTE: Associations of interest do exist in the submission, are not being found below -# # Okay, I've found the problem, at last, the problem is that only one RSL number is saved for each sample, -# # Even though each instance of say "25-YUL13-PU3-0320" has multiple RSL numbers in the excel sheet. -# # so, yeah, the submitters need to make sure that there are unique values for each one. -# try: -# sample_dict = next(item for item in pcr_samples if item['sample'] == assoc.sample.rsl_number -# and item['row'] == assoc.row and item['column'] == assoc.column) -# logger.debug( -# f"Found sample {sample_dict} at index {pcr_samples.index(sample_dict)}: {pcr_samples[pcr_samples.index(sample_dict)]}") -# except StopIteration: -# logger.error(f"Couldn't find {assoc} in the Parser samples") -# continue -# logger.debug(f"Length of pcr_samples: {len(pcr_samples)}") -# assoc = self.update_subsampassoc(assoc=assoc, input_dict=sample_dict) -# result = assoc.save() -# if result: -# report.add_result(result) -# controltype = ControlType.query(name="PCR Control") -# submitted_date = datetime.strptime(" ".join(parser.pcr_info['run_start_date/time'].split(" ")[:-1]), -# "%Y-%m-%d %I:%M:%S %p") -# for control in pcr_controls: -# # logger.debug(f"Control coming into save: {control}") -# new_control = PCRControl(**control) -# new_control.submitted_date = submitted_date -# new_control.controltype = controltype -# new_control.submission = self -# # logger.debug(f"Control coming into save: {new_control.__dict__}") -# new_control.save() -# return report -# -# -# class WastewaterArtic(BasicSubmission): -# """ -# derivative submission type for artic wastewater -# """ -# id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True) -# artic_technician = Column(String(64)) #: Name of technician performing artic -# dna_core_submission_number = Column(String(64)) #: Number used by core as id -# pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic) -# gel_image = Column(String(64)) #: file name of gel image in zip file -# gel_info = Column(JSON) #: unstructured data from gel. -# gel_controls = Column(JSON) #: locations of controls on the gel -# source_plates = Column(JSON) #: wastewater plates that samples come from -# artic_date = Column(TIMESTAMP) #: Date Artic Performed -# ngs_date = Column(TIMESTAMP) #: Date submission received -# gel_date = Column(TIMESTAMP) #: Date submission received -# gel_barcode = Column(String(16)) #: Identifier for the used gel. -# -# __mapper_args__ = dict(polymorphic_identity="Wastewater Artic", -# polymorphic_load="inline", -# inherit_condition=(id == BasicSubmission.id)) -# -# def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict: -# """ -# Extends parent class method to add controls to dict -# -# Returns: -# dict: dictionary used in submissions summary -# """ -# output = super().to_dict(full_data=full_data, backup=backup, report=report) -# if report: -# return output -# if self.artic_technician in [None, "None"]: -# output['artic_technician'] = self.technician -# else: -# output['artic_technician'] = self.artic_technician -# output['gel_info'] = self.gel_info -# output['gel_image'] = self.gel_image -# output['dna_core_submission_number'] = self.dna_core_submission_number -# output['source_plates'] = self.source_plates -# output['artic_date'] = self.artic_date or self.submitted_date -# output['ngs_date'] = self.ngs_date or self.submitted_date -# output['gel_date'] = self.gel_date or self.submitted_date -# output['gel_barcode'] = self.gel_barcode -# return output -# -# @classmethod -# def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None, custom_fields: dict = {}) -> dict: -# """ -# Update submission dictionary with class specific information -# -# Args: -# input_dict (dict): Input sample dictionary -# xl (pd.ExcelFile): original xl workbook, used for child classes mostly -# custom_fields: Dictionary of locations, ranges, etc to be used by this function -# -# Returns: -# dict: Updated sample dictionary -# """ -# from backend.validators import RSLNamer -# from openpyxl_image_loader.sheet_image_loader import SheetImageLoader -# -# def scrape_image(wb: Workbook, info_dict: dict) -> Image or None: -# """ -# Pulls image from excel workbook -# -# Args: -# wb (Workbook): Workbook of interest. -# info_dict (dict): Location map. -# -# Returns: -# Image or None: Image of interest. -# """ -# ws = wb[info_dict['sheet']] -# img_loader = SheetImageLoader(ws) -# for ii in range(info_dict['start_row'], info_dict['end_row'] + 1): -# for jj in range(info_dict['start_column'], info_dict['end_column'] + 1): -# cell_str = f"{row_map[jj]}{ii}" -# if img_loader.image_in(cell_str): -# try: -# return img_loader.get(cell_str) -# except ValueError as e: -# logger.error(f"Could not open image from cell: {cell_str} due to {e}") -# return None -# return None -# -# input_dict = super().custom_info_parser(input_dict) -# input_dict['submission_type'] = dict(value="Wastewater Artic", missing=False) -# egel_section = custom_fields['egel_controls'] -# ws = xl[egel_section['sheet']] -# # NOTE: Here we should be scraping the control results. -# data = [ws.cell(row=ii, column=jj) for jj in range(egel_section['start_column'], egel_section['end_column'] + 1) -# for -# ii in range(egel_section['start_row'], egel_section['end_row'] + 1)] -# data = [cell for cell in data if cell.value is not None and "NTC" in cell.value] -# input_dict['gel_controls'] = [ -# dict(sample_id=cell.value, location=f"{row_map[cell.row - 9]}{str(cell.column - 14).zfill(2)}") for cell in -# data] -# # NOTE: Get source plate information -# source_plates_section = custom_fields['source_plates'] -# ws = xl[source_plates_section['sheet']] -# data = [dict(plate=ws.cell(row=ii, column=source_plates_section['plate_column']).value, -# starting_sample=ws.cell(row=ii, column=source_plates_section['starting_sample_column']).value) for -# ii in -# range(source_plates_section['start_row'], source_plates_section['end_row'] + 1)] -# for datum in data: -# if datum['plate'] in ["None", None, ""]: -# continue -# else: -# datum['plate'] = RSLNamer(filename=datum['plate'], submission_type="Wastewater").parsed_name -# if xl is not None: -# try: -# input_dict['csv'] = xl["hitpicks_csv_to_export"] -# except KeyError as e: -# logger.error(e) -# try: -# match input_dict['rsl_plate_num']: -# case dict(): -# input_dict['csv'] = xl[input_dict['rsl_plate_num']['value']] -# case str(): -# input_dict['csv'] = xl[input_dict['rsl_plate_num']] -# case _: -# pass -# except Exception as e: -# logger.error(f"Error handling couldn't get csv due to: {e}") -# input_dict['source_plates'] = data -# egel_info_section = custom_fields['egel_info'] -# ws = xl[egel_info_section['sheet']] -# data = [] -# for ii in range(egel_info_section['start_row'], egel_info_section['end_row'] + 1): -# datum = dict( -# name=ws.cell(row=ii, column=egel_info_section['start_column'] - 3).value, -# values=[] -# ) -# for jj in range(egel_info_section['start_column'], egel_info_section['end_column'] + 1): -# d = dict( -# name=ws.cell(row=egel_info_section['start_row'] - 1, column=jj).value, -# value=ws.cell(row=ii, column=jj).value -# ) -# if d['value'] is not None: -# datum['values'].append(d) -# data.append(datum) -# input_dict['gel_info'] = data -# egel_image_section = custom_fields['image_range'] -# img: Image = scrape_image(wb=xl, info_dict=egel_image_section) -# if img is not None: -# tmp = Path(TemporaryFile().name).with_suffix(".jpg") -# img.save(tmp.__str__()) -# with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf: -# # NOTE: Add a file located at the source_path to the destination within the zip -# # file. It will overwrite existing files if the names collide, but it -# # will give a warning -# zipf.write(tmp.__str__(), f"{input_dict['rsl_plate_num']['value']}.jpg") -# input_dict['gel_image'] = f"{input_dict['rsl_plate_num']['value']}.jpg" -# return input_dict -# -# @classmethod -# def enforce_name(cls, instr: str, data: dict = {}) -> str: -# """ -# Custom naming method for this class. Extends parent. -# -# Args: -# instr (str): Initial name. -# data (dict | None, optional): Additional parameters for name. Defaults to None. -# -# Returns: -# str: Updated name. -# """ -# logger.debug(f"Incoming String: {instr}") -# try: -# # NOTE: Deal with PCR file. -# instr = re.sub(r"Artic", "", instr, flags=re.IGNORECASE) -# except (AttributeError, TypeError) as e: -# logger.error(f"Problem using regex: {e}") -# try: -# instr = instr.replace("-", "") -# except AttributeError: -# instr = date.today().strftime("%Y%m%d") -# instr = re.sub(r"^(\d{6})", f"RSL-AR-\\1", instr) -# outstr = super().enforce_name(instr=instr, data=data) -# outstr = outstr.replace("RSLAR", "RSL-AR") -# return outstr -# -# @classmethod -# def parse_samples(cls, input_dict: dict) -> dict: -# """ -# Update sample dictionary with type specific information. Extends parent. -# -# Args: -# input_dict (dict): Input sample dictionary -# -# Returns: -# dict: Updated sample dictionary -# """ -# input_dict = super().parse_samples(input_dict) -# input_dict['sample_type'] = "Wastewater Sample" -# # NOTE: Stop gap solution because WW is sloppy with their naming schemes -# try: -# input_dict['source_plate'] = input_dict['source_plate'].replace("WW20", "WW-20") -# except KeyError: -# pass -# try: -# input_dict['source_plate_number'] = int(input_dict['source_plate_number']) -# except (ValueError, KeyError): -# input_dict['source_plate_number'] = 0 -# # NOTE: Because generate_sample_object needs the submitter_id and the artic has the "({origin well})" at the end, this has to be done here. No moving to sqlalchemy object :( -# input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip() -# try: -# input_dict['ww_processing_num'] = input_dict['sample_name_(lims)'] -# del input_dict['sample_name_(lims)'] -# except KeyError: -# logger.error(f"Unable to set ww_processing_num for sample {input_dict['submitter_id']}") -# try: -# input_dict['ww_full_sample_id'] = input_dict['sample_name_(ww)'] -# del input_dict['sample_name_(ww)'] -# except KeyError: -# logger.error(f"Unable to set ww_processing_num for sample {input_dict['submitter_id']}") -# year = str(date.today().year)[-2:] -# # NOTE: Check for extraction negative control (Enterics) -# if re.search(rf"^{year}-(ENC)", input_dict['submitter_id']): -# input_dict['rsl_number'] = cls.en_adapter(input_str=input_dict['submitter_id']) -# # NOTE: Check for extraction negative control (Robotics) -# if re.search(rf"^{year}-(RSL)", input_dict['submitter_id']): -# logger.debug(f"Found {year}-(RSL), so we are going to run PBS adapter:") -# input_dict['rsl_number'] = cls.pbs_adapter(input_str=input_dict['submitter_id']) -# return input_dict -# -# @classmethod -# def en_adapter(cls, input_str: str) -> str: -# """ -# Stopgap solution because WW names their ENs different -# -# Args: -# input_str (str): input name -# -# Returns: -# str: output name -# """ -# processed = input_str.replace("RSL", "") -# # NOTE: Remove anything in brackets at the end of string? -# processed = re.sub(r"\(.*\)$", "", processed).strip() -# # NOTE: Remove letters that are not R. -# processed = re.sub(r"[A-QS-Z]+\d*", "", processed) -# # NOTE: Remove trailing '-' if any -# processed = processed.strip("-") -# try: -# # NOTE: get digit at the end of the string. -# en_num = re.search(r"\-\d{1}$", processed).group() -# processed = rreplace(processed, en_num, "") -# except AttributeError: -# en_num = "1" -# en_num = en_num.strip("-") -# try: -# # NOTE: Get last digit and maybe 'R' with another digit. -# plate_num = re.search(r"\-\d{1}R?\d?$", processed).group() -# processed = rreplace(processed, plate_num, "") -# except AttributeError: -# plate_num = "1" -# # NOTE: plate_num not currently used, but will keep incase it is in the future -# plate_num = plate_num.strip("-") -# day = re.search(r"\d{2}$", processed).group() -# processed = rreplace(processed, day, "") -# month = re.search(r"\d{2}$", processed).group() -# processed = rreplace(processed, month, "") -# processed = processed.replace("--", "") -# year = re.search(r'^(?:\d{2})?\d{2}', processed).group() -# year = f"20{year}" -# final_en_name = f"EN{en_num}-{year}{month}{day}" -# return final_en_name -# -# @classmethod -# def pbs_adapter(cls, input_str): -# """ -# Stopgap solution because WW names their controls different -# -# Args: -# input_str (str): input name -# -# Returns: -# str: output name -# """ -# logger.debug(f"PBS adapter on {input_str}") -# # NOTE: Remove letters. -# processed = input_str.replace("RSL", "") -# # logger.debug(processed) -# # NOTE: Remove brackets at end -# processed = re.sub(r"\(.*\)$", "", processed).strip() -# # logger.debug(processed) -# processed = re.sub(r"-RPT", "", processed, flags=re.IGNORECASE) -# # NOTE: Remove any non-R letters at end. -# processed = re.sub(r"[A-QS-Z]+\d*", "", processed) -# # logger.debug(processed) -# # NOTE: Remove trailing '-' if any -# processed = processed.strip("-") -# # logger.debug(processed) -# try: -# plate_num = re.search(r"\-\d{1}R?\d?$", processed).group() -# processed = rreplace(processed, plate_num, "") -# except AttributeError: -# plate_num = "1" -# plate_num = plate_num.strip("-") -# try: -# repeat_num = re.search(r"R(?P\d)?$", processed).groups()[0] -# except: -# repeat_num = None -# if repeat_num is None and "R" in plate_num: -# repeat_num = "1" -# try: -# plate_num = re.sub(r"R", rf"R{repeat_num}", plate_num) -# except AttributeError: -# logger.error(f"Problem re-evaluating plate number for {processed}") -# # logger.debug(processed) -# # NOTE: Remove any redundant -digits -# processed = re.sub(r"-\d$", "", processed) -# # logger.debug(processed) -# day = re.search(r"\d{2}$", processed).group() -# processed = rreplace(processed, day, "") -# # logger.debug(processed) -# month = re.search(r"\d{2}$", processed).group() -# processed = rreplace(processed, month, "") -# processed = processed.replace("--", "") -# # logger.debug(processed) -# year = re.search(r'^(?:\d{2})?\d{2}', processed).group() -# year = f"20{year}" -# # logger.debug(processed) -# final_en_name = f"PBS{year}{month}{day}-{plate_num}" -# return final_en_name -# -# @classmethod -# def custom_validation(cls, pyd) -> dict: -# """ -# Performs any final custom parsing of the excel file. Extends parent -# -# Args: -# input_dict (dict): Parser product up to this point. -# xl (pd.ExcelFile | None, optional): Excel submission form. Defaults to None. -# info_map (dict | None, optional): Map of information locations from SubmissionType. Defaults to None. -# plate_map (dict | None, optional): Constructed plate map of samples. Defaults to None. -# -# Returns: -# dict: Updated parser product. -# """ -# input_dict = super().custom_validation(pyd) -# exclude_plates = [None, "", "none", "na"] -# pyd.source_plates = [plate for plate in pyd.source_plates if plate['plate'].lower() not in exclude_plates] -# for sample in pyd.samples: -# if re.search(r"^NTC", sample.submitter_id): -# if isinstance(pyd.rsl_plate_num, dict): -# placeholder = pyd.rsl_plate_num['value'] -# else: -# placeholder = pyd.rsl_plate_num -# sample.submitter_id = f"{sample.submitter_id}-WWG-{placeholder}" -# return input_dict -# -# @classmethod -# def custom_info_writer(cls, input_excel: Workbook, info: dict | None = None, backup: bool = False, -# custom_fields: dict = {}) -> Workbook: -# """ -# Adds custom autofill methods for submission. Extends Parent -# -# Args: -# input_excel (Workbook): initial workbook. -# info (dict | None, optional): dictionary of additional info. Defaults to None. -# backup (bool, optional): Whether this is part of a backup operation. Defaults to False. -# custom_fields: Dictionary of locations, ranges, etc to be used by this function -# -# Returns: -# Workbook: Updated workbook -# """ -# input_excel = super().custom_info_writer(input_excel, info, backup) -# if isinstance(info, types.GeneratorType): -# info = {k: v for k, v in info} -# # NOTE: check for source plate information -# if check_key_or_attr(key='source_plates', interest=info, check_none=True): -# source_plates_section = custom_fields['source_plates'] -# worksheet = input_excel[source_plates_section['sheet']] -# start_row = source_plates_section['start_row'] -# # NOTE: write source plates to First strand list -# for iii, plate in enumerate(info['source_plates']['value']): -# row = start_row + iii -# try: -# worksheet.cell(row=row, column=source_plates_section['plate_column'], value=plate['plate']) -# except TypeError: -# pass -# try: -# worksheet.cell(row=row, column=source_plates_section['starting_sample_column'], -# value=plate['starting_sample']) -# except TypeError: -# pass -# else: -# logger.warning(f"No source plate info found.") -# # NOTE: check for gel information -# if check_key_or_attr(key='gel_info', interest=info, check_none=True): -# egel_section = custom_fields['egel_info'] -# # NOTE: print json field gel results to Egel results -# worksheet = input_excel[egel_section['sheet']] -# start_row = egel_section['start_row'] - 1 -# start_column = egel_section['start_column'] - 3 -# for row, ki in enumerate(info['gel_info']['value'], start=1): -# row = start_row + row -# worksheet.cell(row=row, column=start_column, value=ki['name']) -# for jjj, kj in enumerate(ki['values'], start=1): -# column = start_column + 2 + jjj -# worksheet.cell(row=start_row, column=column, value=kj['name']) -# try: -# worksheet.cell(row=row, column=column, value=kj['value']) -# except AttributeError: -# logger.error(f"Failed {kj['name']} with value {kj['value']} to row {row}, column {column}") -# else: -# logger.warning("No gel info found.") -# if check_key_or_attr(key='gel_image', interest=info, check_none=True): -# worksheet = input_excel[egel_section['sheet']] -# with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped: -# z = zipped.extract(info['gel_image']['value'], Path(TemporaryDirectory().name)) -# img = OpenpyxlImage(z) -# img.height = 400 # insert image height in pixels as float or int (e.g. 305.5) -# img.width = 600 -# img.anchor = egel_section['img_anchor'] -# worksheet.add_image(img) -# else: -# logger.warning("No gel image found.") -# return input_excel -# -# @classmethod -# def custom_sample_writer(self, sample: dict) -> dict: -# if sample['source_plate_number'] in [0, "0"]: -# sample['source_plate_number'] = "control" -# return sample -# -# @classmethod -# def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]: -# """ -# Get the details jinja template for the correct class. Extends parent -# -# Args: -# base_dict (dict): incoming dictionary of Submission fields -# -# Returns: -# Tuple[dict, Template]: (Updated dictionary, Template to be rendered) -# """ -# base_dict, template = super().get_details_template(base_dict=base_dict) -# base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number", "source_plates", -# "gel_controls, gel_image_path"] -# base_dict['DNA Core ID'] = base_dict['dna_core_submission_number'] -# if check_key_or_attr(key='gel_info', interest=base_dict, check_none=True): -# headers = [item['name'] for item in base_dict['gel_info'][0]['values']] -# base_dict['headers'] = [''] * (4 - len(headers)) -# base_dict['headers'] += headers -# if check_key_or_attr(key='gel_image', interest=base_dict, check_none=True): -# with ZipFile(cls.__directory_path__.joinpath("submission_imgs.zip")) as zipped: -# base_dict['gel_image_actual'] = base64.b64encode(zipped.read(base_dict['gel_image'])).decode('utf-8') -# return base_dict, template -# -# def custom_context_events(self) -> dict: -# """ -# Creates dictionary of str:function to be passed to context menu. Extends parent -# -# Returns: -# dict: dictionary of functions -# """ -# events = super().custom_context_events() -# events['Gel Box'] = self.gel_box -# return events -# -# def set_attribute(self, key: str, value): -# """ -# Performs custom attribute setting based on values. Extends parent -# -# Args: -# key (str): name of attribute -# value (_type_): value of attribute -# """ -# super().set_attribute(key=key, value=value) -# if key == 'gel_info': -# if len(self.gel_info) > 3: -# self.gel_info = self.gel_info[-3:] -# -# def gel_box(self, obj): -# """ -# Creates PYQT6 widget to perform gel viewing operations -# -# Args: -# obj (_type_): parent widget -# """ -# from frontend.widgets.gel_checker import GelBox -# from frontend.widgets import select_open_file -# report = Report() -# fname = select_open_file(obj=obj, file_extension="jpg") -# if not fname: -# report.add_result(Result(msg="No file selected, cancelling.", status="Warning")) -# return report -# dlg = GelBox(parent=obj, img_path=fname, submission=self) -# if dlg.exec(): -# self.dna_core_submission_number, self.gel_barcode, img_path, output, comment = dlg.parse_form() -# self.gel_image = img_path.name -# self.gel_info = output -# dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S") -# com = dict(text=comment, name=getuser(), time=dt) -# if com['text'] is not None and com['text'] != "": -# if self.comment is not None: -# self.comment.append(com) -# else: -# self.comment = [com] -# with ZipFile(self.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf: -# # NOTE: Add a file located at the source_path to the destination within the zip -# # file. It will overwrite existing files if the names collide, but it -# # will give a warning -# zipf.write(img_path, self.gel_image) -# self.save() -# # NOTE: Sample Classes @@ -2668,7 +1641,7 @@ class BasicSample(BaseClass, LogMixin): id = Column(INTEGER, primary_key=True) #: primary key submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter sample_type = Column(String(32)) #: mode_sub_type of sample - misc_info = Column(JSON) + # misc_info = Column(JSON) control = relationship("Control", back_populates="sample", uselist=False) sample_submission_associations = relationship( @@ -2677,20 +1650,15 @@ class BasicSample(BaseClass, LogMixin): cascade="all, delete-orphan", ) #: associated submissions - __mapper_args__ = { - "polymorphic_identity": "Basic Sample", - "polymorphic_on": case( + submissions = association_proxy("sample_submission_associations", "run") #: proxy of associated submissions - (sample_type == "Wastewater Sample", "Wastewater Sample"), - (sample_type == "Wastewater Artic Sample", "Wastewater Sample"), - (sample_type == "Bacterial Culture Sample", "Bacterial Culture Sample"), + sample_run_associations = relationship( + "RunSampleAssociation", + back_populates="sample", + cascade="all, delete-orphan", + ) #: associated submissions - else_="Basic Sample" - ), - "with_polymorphic": "*", - } - - submissions = association_proxy("sample_submission_associations", "submission") #: proxy of associated submissions + submissions = association_proxy("sample_submission_associations", "run") #: proxy of associated submissions @validates('submitter_id') def create_id(self, key: str, value: str) -> str: @@ -2881,7 +1849,7 @@ class BasicSample(BaseClass, LogMixin): sample_type (str): sample subclass name Raises: - ValueError: Raised if no kwargs are passed to narrow down instances + ValueError: Raised if no kwargs are passed to narrow down controls ValueError: Raised if unallowed key is given. Returns: @@ -2960,7 +1928,7 @@ class BasicSample(BaseClass, LogMixin): def show_details(self, obj): """ - Creates Widget for showing submission details. + Creates Widget for showing run details. Args: obj (_type_): parent widget @@ -2984,176 +1952,31 @@ class BasicSample(BaseClass, LogMixin): self.show_details(obj) -# NOTE: Below are the custom sample types +# NOTE: Submission to Sample Associations -# class WastewaterSample(BasicSample): -# """ -# Derivative wastewater sample -# """ -# -# id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True) -# ww_processing_num = Column(String(64)) #: wastewater processing number -# ww_full_sample_id = Column(String(64)) #: full id given by entrics -# rsl_number = Column(String(64)) #: rsl plate identification number -# collection_date = Column(TIMESTAMP) #: Date sample collected -# received_date = Column(TIMESTAMP) #: Date sample received -# notes = Column(String(2000)) #: notes from submission form -# sample_location = Column(String(8)) #: location on 24 well plate -# __mapper_args__ = dict(polymorphic_identity="Wastewater Sample", -# polymorphic_load="inline", -# inherit_condition=(id == BasicSample.id)) -# -# @classmethod -# def get_default_info(cls, *args): -# """ -# Returns default info for a model. Extends BaseClass method. -# -# Returns: -# dict | list | str: Output of key:value dict or single (list, str) desired variable -# """ -# dicto = super().get_default_info(*args) -# match dicto: -# case dict(): -# dicto['singles'] += ['ww_processing_num'] -# output = {} -# for k, v in dicto.items(): -# if len(args) > 0 and k not in args: -# continue -# else: -# output[k] = v -# if len(args) == 1: -# return output[args[0]] -# case list(): -# if "singles" in args: -# dicto += ['ww_processing_num'] -# return dicto -# case _: -# pass -# -# def to_sub_dict(self, full_data: bool = False) -> dict: -# """ -# gui friendly dictionary, extends parent method. -# -# Returns: -# dict: sample id, type, received date, collection date -# """ -# sample = super().to_sub_dict(full_data=full_data) -# sample['ww_processing_num'] = self.ww_processing_num -# sample['sample_location'] = self.sample_location -# sample['received_date'] = self.received_date -# sample['collection_date'] = self.collection_date -# return sample -# -# @classmethod -# def parse_sample(cls, input_dict: dict) -> dict: -# """ -# Custom sample parser. Extends parent -# -# Args: -# input_dict (dict): Basic parser results for this sample. -# -# Returns: -# dict: Updated parser results. -# """ -# output_dict = super().parse_sample(input_dict) -# disallowed = ["", None, "None"] -# try: -# check = output_dict['rsl_number'] in disallowed -# except KeyError: -# check = True -# if check: -# output_dict['rsl_number'] = "RSL-WW-" + output_dict['ww_processing_num'] -# if output_dict['ww_full_sample_id'] is not None and output_dict["submitter_id"] in disallowed: -# output_dict["submitter_id"] = output_dict['ww_full_sample_id'] -# # check = check_key_or_attr("rsl_number", output_dict, check_none=True) -# return output_dict -# -# @classproperty -# def searchables(cls) -> List[dict]: -# """ -# Delivers a list of fields that can be used in fuzzy search. Extends parent. -# -# Returns: -# List[str]: List of fields. -# """ -# searchables = deepcopy(super().searchables) -# for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]: -# label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title() -# searchables.append(dict(label=label, field=item)) -# return searchables -# -# -# class BacterialCultureSample(BasicSample): -# """ -# base of bacterial culture sample -# """ -# id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True) -# organism = Column(String(64)) #: bacterial specimen -# concentration = Column(String(16)) #: sample concentration -# control = relationship("IridaControl", back_populates="sample", uselist=False) -# __mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample", -# polymorphic_load="inline", -# inherit_condition=(id == BasicSample.id)) -# -# def to_sub_dict(self, full_data: bool = False) -> dict: -# """ -# gui friendly dictionary, extends parent method. -# -# Returns: -# dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above -# """ -# sample = super().to_sub_dict(full_data=full_data) -# sample['name'] = self.submitter_id -# sample['organism'] = self.organism -# try: -# sample['concentration'] = f"{float(self.concentration):.2f}" -# except (TypeError, ValueError): -# sample['concentration'] = 0.0 -# if self.control is not None: -# sample['colour'] = [0, 128, 0] -# target = next((v for k, v in self.control.controltype.targets.items() if k == self.control.subtype), -# "Not Available") -# try: -# target = ", ".join(target) -# except: -# target = "None" -# sample['tooltip'] = f"\nControl: {self.control.controltype.name} - {target}" -# return sample -# - -# # NOTE: Submission to Sample Associations class SubmissionSampleAssociation(BaseClass): """ - table containing submission/sample associations + table containing run/sample associations DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html """ id = Column(INTEGER, unique=True, nullable=False) #: id to be used for inheriting purposes sample_id = Column(INTEGER, ForeignKey("_basicsample.id"), nullable=False) #: id of associated sample - submission_id = Column(INTEGER, ForeignKey("_clientsubmission.id"), primary_key=True) #: id of associated submission + submission_id = Column(INTEGER, ForeignKey("_clientsubmission.id"), primary_key=True) #: id of associated run row = Column(INTEGER, primary_key=True) #: row on the 96 well plate column = Column(INTEGER, primary_key=True) #: column on the 96 well plate submission_rank = Column(INTEGER, nullable=False, default=0) #: Location in sample list - misc_info = Column(JSON) + # misc_info = Column(JSON) # NOTE: reference to the Submission object submission = relationship(ClientSubmission, - back_populates="submission_sample_associations") #: associated submission + back_populates="submission_sample_associations") #: associated run # NOTE: reference to the Sample object sample = relationship(BasicSample, back_populates="sample_submission_associations") #: associated sample - # base_sub_type = Column(String) #: string of mode_sub_type name - - # NOTE: Refers to the type of parent. - # __mapper_args__ = { - # "polymorphic_identity": "Basic Association", - # "polymorphic_on": base_sub_type, - # "with_polymorphic": "*", - # } - - def __init__(self, submission: BasicSubmission = None, sample: BasicSample = None, row: int = 1, column: int = 1, + def __init__(self, submission: ClientSubmission = None, sample: BasicSample = None, row: int = 1, column: int = 1, id: int | None = None, submission_rank: int = 0, **kwargs): self.submission = submission self.sample = sample @@ -3283,7 +2106,7 @@ class SubmissionSampleAssociation(BaseClass): @classmethod @setup_lookup def query(cls, - submission: BasicSubmission | str | None = None, + submission: ClientSubmission | str | None = None, exclude_submission_type: str | None = None, sample: BasicSample | str | None = None, row: int = 0, @@ -3297,10 +2120,10 @@ class SubmissionSampleAssociation(BaseClass): Lookup junction of Submission and Sample in the database Args: - submission (models.BasicSubmission | str | None, optional): Submission of interest. Defaults to None. + run (models.BasicRun | str | None, optional): Submission of interest. Defaults to None. sample (models.BasicSample | str | None, optional): Sample of interest. Defaults to None. - row (int, optional): Row of the sample location on submission plate. Defaults to 0. - column (int, optional): Column of the sample location on the submission plate. Defaults to 0. + row (int, optional): Row of the sample location on run plate. Defaults to 0. + column (int, optional): Column of the sample location on the run plate. Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. chronologic (bool, optional): Return results in chronologic order. Defaults to False. @@ -3309,10 +2132,10 @@ class SubmissionSampleAssociation(BaseClass): """ query: Query = cls.__database_session__.query(cls) match submission: - case BasicSubmission(): + case ClientSubmission(): query = query.filter(cls.submission == submission) case str(): - query = query.join(BasicSubmission).filter(BasicSubmission.rsl_plate_num == submission) + query = query.join(ClientSubmission).filter(ClientSubmission.rsl_plate_num == submission) case _: pass match sample: @@ -3328,23 +2151,23 @@ class SubmissionSampleAssociation(BaseClass): query = query.filter(cls.column == column) match exclude_submission_type: case str(): - query = query.join(BasicSubmission).filter( - BasicSubmission.submission_type_name != exclude_submission_type) + query = query.join(BasicRun).filter( + BasicRun.submission_type_name != exclude_submission_type) case _: pass if reverse and not chronologic: - query = query.order_by(BasicSubmission.id.desc()) + query = query.order_by(BasicRun.id.desc()) if chronologic: if reverse: - query = query.order_by(BasicSubmission.submitted_date.desc()) + query = query.order_by(ClientSubmission.submitted_date.desc()) else: - query = query.order_by(BasicSubmission.submitted_date) + query = query.order_by(ClientSubmission.submitted_date) return cls.execute_query(query=query, limit=limit, **kwargs) @classmethod def query_or_create(cls, association_type: str = "Basic Association", - submission: BasicSubmission | str | None = None, + submission: ClientSubmission | str | None = None, sample: BasicSample | str | None = None, id: int | None = None, **kwargs) -> SubmissionSampleAssociation: @@ -3353,7 +2176,7 @@ class SubmissionSampleAssociation(BaseClass): Args: association_type (str, optional): Subclass name. Defaults to "Basic Association". - submission (BasicSubmission | str | None, optional): associated submission. Defaults to None. + submission (BasicRun | str | None, optional): associated run. Defaults to None. sample (BasicSample | str | None, optional): associated sample. Defaults to None. id (int | None, optional): association id. Defaults to None. @@ -3361,10 +2184,10 @@ class SubmissionSampleAssociation(BaseClass): SubmissionSampleAssociation: Queried or new association. """ match submission: - case BasicSubmission(): + case BasicRun(): pass case str(): - submission = BasicSubmission.query(rsl_plate_num=submission) + submission = ClientSubmission.query(rsl_plate_num=submission) case _: raise ValueError() match sample: @@ -3395,96 +2218,264 @@ class SubmissionSampleAssociation(BaseClass): raise AttributeError(f"Delete not implemented for {self.__class__}") -# class WastewaterAssociation(SubmissionSampleAssociation): -# """ -# table containing wastewater specific submission/sample associations -# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html -# """ -# id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True) -# ct_n1 = Column(FLOAT(2)) #: AKA ct for N1 -# ct_n2 = Column(FLOAT(2)) #: AKA ct for N2 -# n1_status = Column(String(32)) #: positive or negative for N1 -# n2_status = Column(String(32)) #: positive or negative for N2 -# pcr_results = Column(JSON) #: imported PCR status from QuantStudio -# -# __mapper_args__ = dict(polymorphic_identity="Wastewater Association", -# polymorphic_load="inline", -# inherit_condition=(id == SubmissionSampleAssociation.id)) -# -# def to_sub_dict(self) -> dict: -# """ -# Returns a sample dictionary updated with instance information. Extends parent -# -# Returns: -# dict: Updated dictionary with row, column and well updated -# """ -# -# sample = super().to_sub_dict() -# sample['ct'] = f"({self.ct_n1}, {self.ct_n2})" -# try: -# sample['source_row'] = row_keys[self.sample.sample_location[0]] -# sample['source_column'] = int(self.sample.sample_location[1:]) -# except (TypeError, AttributeError) as e: -# logger.error(f"Couldn't set sources for {self.sample.rsl_number}. Looks like there isn't data.") -# try: -# sample['positive'] = any(["positive" in item for item in [self.n1_status, self.n2_status]]) -# except (TypeError, AttributeError) as e: -# logger.error(f"Couldn't check positives for {self.sample.rsl_number}. Looks like there isn't PCR data.") -# return sample -# -# @property -# def hitpicked(self) -> dict | None: -# """ -# Outputs a dictionary usable for html plate maps. Extends parent -# -# Returns: -# dict: dictionary of sample id, row and column in elution plate -# """ -# sample = super().hitpicked -# try: -# scaler = max([self.ct_n1, self.ct_n2]) -# except TypeError: -# scaler = 0.0 -# if scaler == 0.0: -# scaler = 45 -# bg = (45 - scaler) * 17 -# red = min([64 + bg, 255]) -# grn = max([255 - bg, 0]) -# blu = 128 -# sample['background_color'] = f"rgb({red}, {grn}, {blu})" -# try: -# sample[ -# 'tooltip'] += f"
- ct N1: {'{:.2f}'.format(self.ct_n1)}
- ct N2: {'{:.2f}'.format(self.ct_n2)}" -# except (TypeError, AttributeError) as e: -# logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.") -# return sample -# -# -# class WastewaterArticAssociation(SubmissionSampleAssociation): -# """ -# table containing wastewater artic specific submission/sample associations -# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html -# """ -# id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True) -# source_plate = Column(String(32)) -# source_plate_number = Column(INTEGER) -# source_well = Column(String(8)) -# ct = Column(String(8)) #: AKA ct for N1 -# -# __mapper_args__ = dict(polymorphic_identity="Wastewater Artic Association", -# polymorphic_load="inline", -# inherit_condition=(id == SubmissionSampleAssociation.id)) -# -# def to_sub_dict(self) -> dict: -# """ -# Returns a sample dictionary updated with instance information. Extends parent -# -# Returns: -# dict: Updated dictionary with row, column and well updated -# """ -# sample = super().to_sub_dict() -# sample['ct'] = self.ct -# sample['source_plate'] = self.source_plate -# sample['source_plate_number'] = self.source_plate_number -# sample['source_well'] = self.source_well -# return sample +class RunSampleAssociation(BaseClass): + + """ + table containing run/sample associations + DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html + """ + + id = Column(INTEGER, unique=True, nullable=False) #: id to be used for inheriting purposes + sample_id = Column(INTEGER, ForeignKey("_basicsample.id"), nullable=False) #: id of associated sample + run_id = Column(INTEGER, ForeignKey("_basicrun.id"), primary_key=True) #: id of associated run + row = Column(INTEGER, primary_key=True) #: row on the 96 well plate + column = Column(INTEGER, primary_key=True) #: column on the 96 well plate + # misc_info = Column(JSON) + + # NOTE: reference to the Submission object + + run = relationship(BasicRun, + back_populates="run_sample_associations") #: associated run + + # NOTE: reference to the Sample object + sample = relationship(BasicSample, back_populates="sample_run_associations") #: associated sample + + def __init__(self, run: BasicRun = None, sample: BasicSample = None, row: int = 1, column: int = 1, + id: int | None = None, **kwargs): + self.run = run + self.sample = sample + self.row = row + self.column = column + if id is not None: + self.id = id + else: + self.id = self.__class__.autoincrement_id() + for k, v in kwargs.items(): + try: + self.__setattr__(k, v) + except AttributeError: + logger.error(f"Couldn't set {k} to {v}") + + def __repr__(self) -> str: + try: + return f"<{self.__class__.__name__}({self.submission.rsl_plate_num} & {self.sample.submitter_id})" + except AttributeError as e: + logger.error(f"Unable to construct __repr__ due to: {e}") + return super().__repr__() + + def to_sub_dict(self) -> dict: + """ + Returns a sample dictionary updated with instance information + + Returns: + dict: Updated dictionary with row, column and well updated + """ + # NOTE: Get associated sample info + sample = self.sample.to_sub_dict() + sample['name'] = self.sample.submitter_id + sample['row'] = self.row + sample['column'] = self.column + try: + sample['well'] = f"{row_map[self.row]}{self.column}" + except KeyError as e: + logger.error(f"Unable to find row {self.row} in row_map.") + sample['Well'] = None + sample['plate_name'] = self.run.rsl_plate_num + sample['positive'] = False + return sample + + def to_pydantic(self) -> "PydSample": + """ + Creates a pydantic model for this sample. + + Returns: + PydSample: Pydantic Model + """ + from backend.validators import PydSample + return PydSample(**self.to_sub_dict()) + + @property + def hitpicked(self) -> dict | None: + """ + Outputs a dictionary usable for html plate maps. + + Returns: + dict: dictionary of sample id, row and column in elution plate + """ + # NOTE: Since there is no PCR, negliable result is necessary. + sample = self.to_sub_dict() + env = jinja_template_loading() + template = env.get_template("tooltip.html") + tooltip_text = template.render(fields=sample) + try: + control = self.sample.control + except AttributeError: + control = None + if control is not None: + background = "rgb(128, 203, 196)" + else: + background = "rgb(105, 216, 79)" + try: + tooltip_text += sample['tooltip'] + except KeyError: + pass + sample.update(dict(Name=self.sample.submitter_id[:10], tooltip=tooltip_text, background_color=background)) + return sample + + @classmethod + def autoincrement_id(cls) -> int: + """ + Increments the association id automatically + + Returns: + int: incremented id + """ + if cls.__name__ == "SubmissionSampleAssociation": + model = cls + else: + model = next((base for base in cls.__bases__ if base.__name__ == "SubmissionSampleAssociation"), + SubmissionSampleAssociation) + try: + return max([item.id for item in model.query()]) + 1 + except ValueError as e: + logger.error(f"Problem incrementing id: {e}") + return 1 + + @classmethod + def find_polymorphic_subclass(cls, polymorphic_identity: str | None = None) -> SubmissionSampleAssociation: + """ + Retrieves subclasses of SubmissionSampleAssociation based on type name. + + Args: + polymorphic_identity (str | None, optional): Name of subclass fed to polymorphic identity. Defaults to None. + + Returns: + SubmissionSampleAssociation: Subclass of interest. + """ + if isinstance(polymorphic_identity, dict): + polymorphic_identity = polymorphic_identity['value'] + if polymorphic_identity is None: + model = cls + else: + try: + 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}") + model = cls + return model + + @classmethod + @setup_lookup + def query(cls, + run: BasicRun | str | None = None, + exclude_submission_type: str | None = None, + sample: BasicSample | str | None = None, + row: int = 0, + column: int = 0, + limit: int = 0, + chronologic: bool = False, + reverse: bool = False, + **kwargs + ) -> SubmissionSampleAssociation | List[SubmissionSampleAssociation]: + """ + Lookup junction of Submission and Sample in the database + + Args: + run (models.BasicRun | str | None, optional): Submission of interest. Defaults to None. + sample (models.BasicSample | str | None, optional): Sample of interest. Defaults to None. + row (int, optional): Row of the sample location on run plate. Defaults to 0. + column (int, optional): Column of the sample location on the run plate. Defaults to 0. + limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. + chronologic (bool, optional): Return results in chronologic order. Defaults to False. + + Returns: + models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]: Junction(s) of interest + """ + query: Query = cls.__database_session__.query(cls) + match run: + case BasicRun(): + query = query.filter(cls.submission == run) + case str(): + query = query.join(BasicRun).filter(BasicRun.rsl_plate_num == run) + case _: + pass + match sample: + case BasicSample(): + query = query.filter(cls.sample == sample) + case str(): + query = query.join(BasicSample).filter(BasicSample.submitter_id == sample) + case _: + pass + if row > 0: + query = query.filter(cls.row == row) + if column > 0: + query = query.filter(cls.column == column) + match exclude_submission_type: + case str(): + query = query.join(BasicRun).filter( + BasicRun.submission_type_name != exclude_submission_type) + case _: + pass + if reverse and not chronologic: + query = query.order_by(BasicRun.id.desc()) + if chronologic: + if reverse: + query = query.order_by(BasicRun.submitted_date.desc()) + else: + query = query.order_by(BasicRun.submitted_date) + return cls.execute_query(query=query, limit=limit, **kwargs) + + @classmethod + def query_or_create(cls, + association_type: str = "Basic Association", + run: BasicRun | str | None = None, + sample: BasicSample | str | None = None, + id: int | None = None, + **kwargs) -> SubmissionSampleAssociation: + """ + Queries for an association, if none exists creates a new one. + + Args: + association_type (str, optional): Subclass name. Defaults to "Basic Association". + run (BasicRun | str | None, optional): associated run. Defaults to None. + sample (BasicSample | str | None, optional): associated sample. Defaults to None. + id (int | None, optional): association id. Defaults to None. + + Returns: + SubmissionSampleAssociation: Queried or new association. + """ + match run: + case BasicRun(): + pass + case str(): + run = BasicRun.query(rsl_plate_num=run) + case _: + raise ValueError() + match sample: + case BasicSample(): + pass + case str(): + sample = BasicSample.query(submitter_id=sample) + case _: + raise ValueError() + try: + row = kwargs['row'] + except KeyError: + row = None + try: + column = kwargs['column'] + except KeyError: + column = None + try: + instance = cls.query(run=run, sample=sample, row=row, column=column, limit=1) + except StatementError: + instance = None + if instance is None: + used_cls = cls.find_polymorphic_subclass(polymorphic_identity=association_type) + instance = used_cls(run=run, sample=sample, id=id, **kwargs) + return instance + + def delete(self): + raise AttributeError(f"Delete not implemented for {self.__class__}") + + diff --git a/src/submissions/backend/excel/__init__.py b/src/submissions/backend/excel/__init__.py index 92009f2..22f5820 100644 --- a/src/submissions/backend/excel/__init__.py +++ b/src/submissions/backend/excel/__init__.py @@ -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 * diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index e553348..8f7df5f 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -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) diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 0d526b7..47fe044 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -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) diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 49b1c90..e310960 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -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): diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index 17047cb..6f0c8e4 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -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. diff --git a/src/submissions/backend/validators/omni_gui_objects.py b/src/submissions/backend/validators/omni_gui_objects.py index 9be55af..e0c5b68 100644 --- a/src/submissions/backend/validators/omni_gui_objects.py +++ b/src/submissions/backend/validators/omni_gui_objects.py @@ -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 diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 7913332..823eed0 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -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') diff --git a/src/submissions/frontend/visualizations/concentrations_chart.py b/src/submissions/frontend/visualizations/concentrations_chart.py index 9a4610a..3034fca 100644 --- a/src/submissions/frontend/visualizations/concentrations_chart.py +++ b/src/submissions/frontend/visualizations/concentrations_chart.py @@ -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( diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index b3e8701..f4267d4 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -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) diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index b575d99..ba1df9d 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -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}") diff --git a/src/submissions/frontend/widgets/gel_checker.py b/src/submissions/frontend/widgets/gel_checker.py index 54c8f77..f443cca 100644 --- a/src/submissions/frontend/widgets/gel_checker.py +++ b/src/submissions/frontend/widgets/gel_checker.py @@ -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}") diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 47abc89..3dc3364 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -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() diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index f635e20..9e4cb99 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -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): diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index fabb023..2662dd8 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -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): diff --git a/src/submissions/frontend/widgets/summary.py b/src/submissions/frontend/widgets/summary.py index 0df5aef..3c3fa55 100644 --- a/src/submissions/frontend/widgets/summary.py +++ b/src/submissions/frontend/widgets/summary.py @@ -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: diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicrun_details.html similarity index 100% rename from src/submissions/templates/basicsubmission_details.html rename to src/submissions/templates/basicrun_details.html