diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 776c7cc..793a171 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -612,8 +612,8 @@ class BaseClass(Base): relevant = {k: v for k, v in self.__class__.__dict__.items() if isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)} - output = OrderedDict() - output['excluded'] = ["excluded", "misc_info", "_misc_info", "id"] + # output = OrderedDict() + output = dict(excluded = ["excluded", "misc_info", "_misc_info", "id"]) for k, v in relevant.items(): try: check = v.foreign_keys @@ -625,11 +625,16 @@ class BaseClass(Base): value = getattr(self, k) except AttributeError: continue + # try: + # logger.debug(f"Setting {k} to {value} for details dict.") + # except AttributeError as e: + # logger.error(f"Can't log {k} value due to {type(e)}") + # continue output[k.strip("_")] = value if self._misc_info: for key, value in self._misc_info.items(): + # logger.debug(f"Misc info key {key}") output[key] = value - return output @classmethod @@ -750,7 +755,7 @@ class ConfigItem(BaseClass): from .controls import * # NOTE: import order must go: orgs, kittype, run due to circular import issues from .organizations import * -from .kits import * +from .procedures import * from .submissions import * from .audit import AuditLog diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/procedures.py similarity index 67% rename from src/submissions/backend/db/models/kits.py rename to src/submissions/backend/db/models/procedures.py index 419d06a..87ae3f7 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/procedures.py @@ -7,6 +7,7 @@ from operator import itemgetter from pprint import pformat import numpy as np from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.ext.associationproxy import association_proxy from datetime import date, datetime, timedelta @@ -59,13 +60,13 @@ equipmentrole_process = Table( extend_existing=True ) -kittype_process = Table( - "_kittype_process", - Base.metadata, - Column("process_id", INTEGER, ForeignKey("_process.id")), - Column("kittype_id", INTEGER, ForeignKey("_kittype.id")), - extend_existing=True -) +# kittype_process = Table( +# "_kittype_process", +# Base.metadata, +# Column("process_id", INTEGER, ForeignKey("_process.id")), +# Column("kittype_id", INTEGER, ForeignKey("_kittype.id")), +# extend_existing=True +# ) tiprole_tips = Table( "_tiprole_tips", @@ -91,13 +92,13 @@ equipment_tips = Table( extend_existing=True ) -kittype_procedure = Table( - "_kittype_procedure", - Base.metadata, - Column("procedure_id", INTEGER, ForeignKey("_procedure.id")), - Column("kittype_id", INTEGER, ForeignKey("_kittype.id")), - extend_existing=True -) +# kittype_procedure = Table( +# "_kittype_procedure", +# Base.metadata, +# Column("procedure_id", INTEGER, ForeignKey("_procedure.id")), +# Column("kittype_id", INTEGER, ForeignKey("_kittype.id")), +# extend_existing=True +# ) proceduretype_process = Table( "_proceduretype_process", @@ -116,333 +117,333 @@ submissiontype_proceduretype = Table( ) -class KitType(BaseClass): - """ - Base of kits used in procedure processing - """ +# class KitType(BaseClass): +# """ +# Base of kits used in procedure processing +# """ +# +# omni_sort = BaseClass.omni_sort + ["kittypesubmissiontypeassociations", "kittypereagentroleassociation", +# "process"] +# +# id = Column(INTEGER, primary_key=True) #: primary key +# name = Column(String(64), unique=True) #: name of kittype +# procedure = relationship("Procedure", back_populates="kittype", +# secondary=kittype_procedure) #: run this kittype was used for +# process = relationship("Process", back_populates="kittype", +# secondary=kittype_process) #: equipment process used by this kittype +# +# proceduretypeequipmentroleassociation = relationship("ProcedureTypeEquipmentRoleAssociation", back_populates="kittype", +# cascade="all, delete-orphan",) +# +# equipmentrole = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole") +# +# kittypereagentroleassociation = relationship( +# "KitTypeReagentRoleAssociation", +# back_populates="kittype", +# cascade="all, delete-orphan", +# ) +# +# # NOTE: creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 +# reagentrole = association_proxy("kittypereagentroleassociation", "reagentrole", +# creator=lambda RT: KitTypeReagentRoleAssociation( +# reagentrole=RT)) #: Association proxy to KitTypeReagentRoleAssociation +# +# kittypeproceduretypeassociation = relationship( +# "ProcedureTypeKitTypeAssociation", +# back_populates="kittype", +# cascade="all, delete-orphan", +# ) #: Relation to SubmissionType +# +# proceduretype = association_proxy("kittypeproceduretypeassociation", "proceduretype", +# creator=lambda PT: ProcedureTypeKitTypeAssociation( +# proceduretype=PT)) #: Association proxy to SubmissionTypeKitTypeAssociation +# +# +# +# @classproperty +# def aliases(cls) -> List[str]: +# """ +# Gets other names the sql object of this class might go by. +# +# Returns: +# List[str]: List of names +# """ +# return super().aliases + [cls.query_alias, "kittype", "kittype"] +# +# def get_reagents(self, +# required_only: bool = False, +# proceduretype: str | ProcedureType | None = None +# ) -> Generator[ReagentRole, None, None]: +# """ +# Return ReagentTypes linked to kittype through KitTypeReagentTypeAssociation. +# +# Args: +# required_only (bool, optional): If true only return required types. Defaults to False. +# proceduretype (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None. +# +# Returns: +# Generator[ReagentRole, None, None]: List of reagent roles linked to this kittype. +# """ +# match proceduretype: +# case ProcedureType(): +# relevant_associations = [assoc for assoc in self.kittypereagentroleassociation if +# assoc.proceduretype == proceduretype] +# case str(): +# relevant_associations = [assoc for assoc in self.kittypereagentroleassociation if +# assoc.proceduretype.name == proceduretype] +# case _: +# relevant_associations = [assoc for assoc in self.kittypereagentroleassociation] +# if required_only: +# return (assoc.reagentrole for assoc in relevant_associations if assoc.required == 1) +# else: +# return (assoc.reagentrole for assoc in relevant_associations) +# +# def get_equipmentroles(self, proceduretype: str| ProcedureType | None = None) -> Generator[ReagentRole, None, None]: +# match proceduretype: +# case ProcedureType(): +# relevant_associations = [item for item in self.proceduretypeequipmentroleassociation if +# item.proceduretype == proceduretype] +# case str(): +# relevant_associations = [item for item in self.proceduretypeequipmentroleassociation if +# item.proceduretype.name == proceduretype] +# case _: +# relevant_associations = [item for item in self.proceduretypeequipmentroleassociation] +# return (assoc.equipmentrole for assoc in relevant_associations) +# +# +# def construct_xl_map_for_use(self, proceduretype: str | SubmissionType) -> Tuple[dict | None, KitType]: +# """ +# Creates map of locations in Excel workbook for a SubmissionType +# +# Args: +# proceduretype (str | SubmissionType): Submissiontype.name +# +# Returns: +# Generator[(str, str), None, None]: Tuple containing information locations. +# """ +# new_kit = self +# # NOTE: Account for proceduretype variable type. +# match proceduretype: +# case str(): +# # logger.debug(f"Query for {proceduretype}") +# proceduretype = ProcedureType.query(name=proceduretype) +# case SubmissionType(): +# pass +# case _: +# raise ValueError(f"Wrong variable type: {type(proceduretype)} used!") +# # logger.debug(f"Submission type: {proceduretype}, Kit: {self}") +# assocs = [item for item in self.kittypereagentroleassociation if item.proceduretype == proceduretype] +# # logger.debug(f"Associations: {assocs}") +# # NOTE: rescue with procedure type's default kittype. +# if not assocs: +# logger.error( +# f"No associations found with {self}. Attempting rescue with default kittype: {proceduretype.default_kit}") +# new_kit = proceduretype.default_kit +# if not new_kit: +# from frontend.widgets.pop_ups import ObjectSelector +# dlg = ObjectSelector( +# title="Select Kit", +# message="Could not find reagents for this procedure type/kittype type combo.\nSelect new kittype.", +# obj_type=self.__class__, +# values=[kit.name for kit in proceduretype.kittype] +# ) +# if dlg.exec(): +# dlg_result = dlg.parse_form() +# # logger.debug(f"Dialog result: {dlg_result}") +# new_kit = self.__class__.query(name=dlg_result) +# # logger.debug(f"Query result: {new_kit}") +# else: +# return None, new_kit +# assocs = [item for item in new_kit.kittypereagentroleassociation if item.proceduretype == proceduretype] +# output = {assoc.reagentrole.name: assoc.uses for assoc in assocs} +# # logger.debug(f"Output: {output}") +# return output, new_kit +# +# @classmethod +# def query_or_create(cls, **kwargs) -> Tuple[KitType, bool]: +# from backend.validators.pydant import PydKitType +# new = False +# disallowed = ['expiry'] +# sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} +# instance = cls.query(**sanitized_kwargs) +# if not instance or isinstance(instance, list): +# instance = PydKitType(**kwargs) +# new = True +# instance = instance.to_sql() +# logger.info(f"Instance from query or create: {instance}") +# return instance, new +# +# @classmethod +# @setup_lookup +# def query(cls, +# name: str = None, +# proceduretype: str | ProcedureType | None = None, +# id: int | None = None, +# limit: int = 0, +# **kwargs +# ) -> KitType | List[KitType]: +# """ +# Lookup a list of or single KitType. +# +# Args: +# name (str, optional): Name of desired kittype (returns single instance). Defaults to None. +# proceduretype (str | ProcedureType | None, optional): Submission type the kittype is used for. Defaults to None. +# id (int | None, optional): Kit id in the database. Defaults to None. +# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. +# +# Returns: +# KitType|List[KitType]: KitType(s) of interest. +# """ +# query: Query = cls.__database_session__.query(cls) +# match proceduretype: +# case str(): +# query = query.filter(cls.proceduretype.any(name=proceduretype)) +# case ProcedureType(): +# query = query.filter(cls.proceduretype.contains(proceduretype)) +# case _: +# pass +# match name: +# case str(): +# query = query.filter(cls.name == name) +# limit = 1 +# case _: +# pass +# match id: +# case int(): +# query = query.filter(cls.id == id) +# limit = 1 +# case str(): +# query = query.filter(cls.id == int(id)) +# limit = 1 +# case _: +# pass +# return cls.execute_query(query=query, limit=limit, **kwargs) +# +# @check_authorization +# def save(self): +# super().save() - omni_sort = BaseClass.omni_sort + ["kittypesubmissiontypeassociations", "kittypereagentroleassociation", - "process"] +# def to_export_dict(self, proceduretype: SubmissionType) -> dict: +# """ +# Creates dictionary for exporting to yml used in new SubmissionType Construction +# +# Args: +# proceduretype (SubmissionType): SubmissionType of interest. +# +# Returns: +# dict: Dictionary containing relevant info for SubmissionType construction +# """ +# base_dict = dict(name=self.name, reagent_roles=[], equipmentrole=[]) +# for key, value in self.construct_xl_map_for_use(proceduretype=proceduretype): +# try: +# assoc = next(item for item in self.kit_reagentrole_associations if item.reagentrole.name == key) +# except StopIteration as e: +# continue +# for kk, vv in assoc.to_export_dict().items(): +# value[kk] = vv +# base_dict['reagent_roles'].append(value) +# for key, value in proceduretype.construct_field_map("equipment"): +# try: +# assoc = next(item for item in proceduretype.proceduretypeequipmentroleassociation if +# item.equipmentrole.name == key) +# except StopIteration: +# continue +# for kk, vv in assoc.to_export_dict(kittype=self).items(): +# value[kk] = vv +# base_dict['equipmentrole'].append(value) +# return base_dict - id = Column(INTEGER, primary_key=True) #: primary key - name = Column(String(64), unique=True) #: name of kittype - procedure = relationship("Procedure", back_populates="kittype", - secondary=kittype_procedure) #: run this kittype was used for - process = relationship("Process", back_populates="kittype", - secondary=kittype_process) #: equipment process used by this kittype +# @classmethod +# def import_from_yml(cls, proceduretype: str | SubmissionType, filepath: Path | str | None = None, +# import_dict: dict | None = None) -> KitType: +# if isinstance(proceduretype, str): +# proceduretype = SubmissionType.query(name=proceduretype) +# if filepath: +# yaml.add_constructor("!regex", yaml_regex_creator) +# if isinstance(filepath, str): +# filepath = Path(filepath) +# if not filepath.exists(): +# logging.critical(f"Given file could not be found.") +# return None +# with open(filepath, "r") as f: +# if filepath.suffix == ".json": +# import_dict = json.load(fp=f) +# elif filepath.suffix == ".yml": +# import_dict = yaml.load(stream=f, Loader=yaml.Loader) +# else: +# raise Exception(f"Filetype {filepath.suffix} not supported.") +# new_kit = KitType.query(name=import_dict['kittype']['name']) +# if not new_kit: +# new_kit = KitType(name=import_dict['kittype']['name']) +# for reagentrole in import_dict['kittype']['reagent_roles']: +# new_role = ReagentRole.query(name=reagentrole['reagentrole']) +# if new_role: +# check = input(f"Found existing reagentrole: {new_role.name}. Use this? [Y/n]: ") +# if check.lower() == "n": +# new_role = None +# else: +# pass +# if not new_role: +# eol = timedelta(reagentrole['extension_of_life']) +# new_role = ReagentRole(name=reagentrole['reagentrole'], eol_ext=eol) +# uses = dict(expiry=reagentrole['expiry'], lot=reagentrole['lot'], name=reagentrole['name'], sheet=reagentrole['sheet']) +# ktrr_assoc = KitTypeReagentRoleAssociation(kittype=new_kit, reagentrole=new_role, uses=uses) +# ktrr_assoc.proceduretype = proceduretype +# ktrr_assoc.required = reagentrole['required'] +# ktst_assoc = SubmissionTypeKitTypeAssociation( +# kittype=new_kit, +# proceduretype=proceduretype, +# mutable_cost_sample=import_dict['mutable_cost_sample'], +# mutable_cost_column=import_dict['mutable_cost_column'], +# constant_cost=import_dict['constant_cost'] +# ) +# for reagentrole in import_dict['kittype']['equipmentrole']: +# new_role = EquipmentRole.query(name=reagentrole['reagentrole']) +# if new_role: +# check = input(f"Found existing reagentrole: {new_role.name}. Use this? [Y/n]: ") +# if check.lower() == "n": +# new_role = None +# else: +# pass +# if not new_role: +# new_role = EquipmentRole(name=reagentrole['reagentrole']) +# for equipment in Equipment.assign_equipment(equipmentrole=new_role): +# new_role.control.append(equipment) +# ster_assoc = ProcedureTypeEquipmentRoleAssociation(proceduretype=proceduretype, +# equipmentrole=new_role) +# try: +# uses = dict(name=reagentrole['name'], process=reagentrole['process'], sheet=reagentrole['sheet'], +# static=reagentrole['static']) +# except KeyError: +# uses = None +# ster_assoc.uses = uses +# for process in reagentrole['process']: +# new_process = Process.query(name=process) +# if not new_process: +# new_process = Process(name=process) +# new_process.proceduretype.append(proceduretype) +# new_process.kittype.append(new_kit) +# new_process.equipmentrole.append(new_role) +# return new_kit - proceduretypeequipmentroleassociation = relationship("ProcedureTypeEquipmentRoleAssociation", back_populates="kittype", - cascade="all, delete-orphan",) - - equipmentrole = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole") - - kittypereagentroleassociation = relationship( - "KitTypeReagentRoleAssociation", - back_populates="kittype", - cascade="all, delete-orphan", - ) - - # NOTE: creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 - reagentrole = association_proxy("kittypereagentroleassociation", "reagentrole", - creator=lambda RT: KitTypeReagentRoleAssociation( - reagentrole=RT)) #: Association proxy to KitTypeReagentRoleAssociation - - kittypeproceduretypeassociation = relationship( - "ProcedureTypeKitTypeAssociation", - back_populates="kittype", - cascade="all, delete-orphan", - ) #: Relation to SubmissionType - - proceduretype = association_proxy("kittypeproceduretypeassociation", "proceduretype", - creator=lambda PT: ProcedureTypeKitTypeAssociation( - proceduretype=PT)) #: Association proxy to SubmissionTypeKitTypeAssociation - - - - @classproperty - def aliases(cls) -> List[str]: - """ - Gets other names the sql object of this class might go by. - - Returns: - List[str]: List of names - """ - return super().aliases + [cls.query_alias, "kittype", "kittype"] - - def get_reagents(self, - required_only: bool = False, - proceduretype: str | ProcedureType | None = None - ) -> Generator[ReagentRole, None, None]: - """ - Return ReagentTypes linked to kittype through KitTypeReagentTypeAssociation. - - Args: - required_only (bool, optional): If true only return required types. Defaults to False. - proceduretype (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None. - - Returns: - Generator[ReagentRole, None, None]: List of reagent roles linked to this kittype. - """ - match proceduretype: - case ProcedureType(): - relevant_associations = [assoc for assoc in self.kittypereagentroleassociation if - assoc.proceduretype == proceduretype] - case str(): - relevant_associations = [assoc for assoc in self.kittypereagentroleassociation if - assoc.proceduretype.name == proceduretype] - case _: - relevant_associations = [assoc for assoc in self.kittypereagentroleassociation] - if required_only: - return (assoc.reagentrole for assoc in relevant_associations if assoc.required == 1) - else: - return (assoc.reagentrole for assoc in relevant_associations) - - def get_equipmentroles(self, proceduretype: str| ProcedureType | None = None) -> Generator[ReagentRole, None, None]: - match proceduretype: - case ProcedureType(): - relevant_associations = [item for item in self.proceduretypeequipmentroleassociation if - item.proceduretype == proceduretype] - case str(): - relevant_associations = [item for item in self.proceduretypeequipmentroleassociation if - item.proceduretype.name == proceduretype] - case _: - relevant_associations = [item for item in self.proceduretypeequipmentroleassociation] - return (assoc.equipmentrole for assoc in relevant_associations) - - - def construct_xl_map_for_use(self, proceduretype: str | SubmissionType) -> Tuple[dict | None, KitType]: - """ - Creates map of locations in Excel workbook for a SubmissionType - - Args: - proceduretype (str | SubmissionType): Submissiontype.name - - Returns: - Generator[(str, str), None, None]: Tuple containing information locations. - """ - new_kit = self - # NOTE: Account for proceduretype variable type. - match proceduretype: - case str(): - # logger.debug(f"Query for {proceduretype}") - proceduretype = ProcedureType.query(name=proceduretype) - case SubmissionType(): - pass - case _: - raise ValueError(f"Wrong variable type: {type(proceduretype)} used!") - # logger.debug(f"Submission type: {proceduretype}, Kit: {self}") - assocs = [item for item in self.kittypereagentroleassociation if item.proceduretype == proceduretype] - # logger.debug(f"Associations: {assocs}") - # NOTE: rescue with procedure type's default kittype. - if not assocs: - logger.error( - f"No associations found with {self}. Attempting rescue with default kittype: {proceduretype.default_kit}") - new_kit = proceduretype.default_kit - if not new_kit: - from frontend.widgets.pop_ups import ObjectSelector - dlg = ObjectSelector( - title="Select Kit", - message="Could not find reagents for this procedure type/kittype type combo.\nSelect new kittype.", - obj_type=self.__class__, - values=[kit.name for kit in proceduretype.kittype] - ) - if dlg.exec(): - dlg_result = dlg.parse_form() - # logger.debug(f"Dialog result: {dlg_result}") - new_kit = self.__class__.query(name=dlg_result) - # logger.debug(f"Query result: {new_kit}") - else: - return None, new_kit - assocs = [item for item in new_kit.kittypereagentroleassociation if item.proceduretype == proceduretype] - output = {assoc.reagentrole.name: assoc.uses for assoc in assocs} - # logger.debug(f"Output: {output}") - return output, new_kit - - @classmethod - def query_or_create(cls, **kwargs) -> Tuple[KitType, bool]: - from backend.validators.pydant import PydKitType - new = False - disallowed = ['expiry'] - sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} - instance = cls.query(**sanitized_kwargs) - if not instance or isinstance(instance, list): - instance = PydKitType(**kwargs) - new = True - instance = instance.to_sql() - logger.info(f"Instance from query or create: {instance}") - return instance, new - - @classmethod - @setup_lookup - def query(cls, - name: str = None, - proceduretype: str | ProcedureType | None = None, - id: int | None = None, - limit: int = 0, - **kwargs - ) -> KitType | List[KitType]: - """ - Lookup a list of or single KitType. - - Args: - name (str, optional): Name of desired kittype (returns single instance). Defaults to None. - proceduretype (str | ProcedureType | None, optional): Submission type the kittype is used for. Defaults to None. - id (int | None, optional): Kit id in the database. Defaults to None. - limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. - - Returns: - KitType|List[KitType]: KitType(s) of interest. - """ - query: Query = cls.__database_session__.query(cls) - match proceduretype: - case str(): - query = query.filter(cls.proceduretype.any(name=proceduretype)) - case ProcedureType(): - query = query.filter(cls.proceduretype.contains(proceduretype)) - case _: - pass - match name: - case str(): - query = query.filter(cls.name == name) - limit = 1 - case _: - pass - match id: - case int(): - query = query.filter(cls.id == id) - limit = 1 - case str(): - query = query.filter(cls.id == int(id)) - limit = 1 - case _: - pass - return cls.execute_query(query=query, limit=limit, **kwargs) - - @check_authorization - def save(self): - super().save() - - # def to_export_dict(self, proceduretype: SubmissionType) -> dict: - # """ - # Creates dictionary for exporting to yml used in new SubmissionType Construction - # - # Args: - # proceduretype (SubmissionType): SubmissionType of interest. - # - # Returns: - # dict: Dictionary containing relevant info for SubmissionType construction - # """ - # base_dict = dict(name=self.name, reagent_roles=[], equipmentrole=[]) - # for key, value in self.construct_xl_map_for_use(proceduretype=proceduretype): - # try: - # assoc = next(item for item in self.kit_reagentrole_associations if item.reagentrole.name == key) - # except StopIteration as e: - # continue - # for kk, vv in assoc.to_export_dict().items(): - # value[kk] = vv - # base_dict['reagent_roles'].append(value) - # for key, value in proceduretype.construct_field_map("equipment"): - # try: - # assoc = next(item for item in proceduretype.proceduretypeequipmentroleassociation if - # item.equipmentrole.name == key) - # except StopIteration: - # continue - # for kk, vv in assoc.to_export_dict(kittype=self).items(): - # value[kk] = vv - # base_dict['equipmentrole'].append(value) - # return base_dict - - # @classmethod - # def import_from_yml(cls, proceduretype: str | SubmissionType, filepath: Path | str | None = None, - # import_dict: dict | None = None) -> KitType: - # if isinstance(proceduretype, str): - # proceduretype = SubmissionType.query(name=proceduretype) - # if filepath: - # yaml.add_constructor("!regex", yaml_regex_creator) - # if isinstance(filepath, str): - # filepath = Path(filepath) - # if not filepath.exists(): - # logging.critical(f"Given file could not be found.") - # return None - # with open(filepath, "r") as f: - # if filepath.suffix == ".json": - # import_dict = json.load(fp=f) - # elif filepath.suffix == ".yml": - # import_dict = yaml.load(stream=f, Loader=yaml.Loader) - # else: - # raise Exception(f"Filetype {filepath.suffix} not supported.") - # new_kit = KitType.query(name=import_dict['kittype']['name']) - # if not new_kit: - # new_kit = KitType(name=import_dict['kittype']['name']) - # for reagentrole in import_dict['kittype']['reagent_roles']: - # new_role = ReagentRole.query(name=reagentrole['reagentrole']) - # if new_role: - # check = input(f"Found existing reagentrole: {new_role.name}. Use this? [Y/n]: ") - # if check.lower() == "n": - # new_role = None - # else: - # pass - # if not new_role: - # eol = timedelta(reagentrole['extension_of_life']) - # new_role = ReagentRole(name=reagentrole['reagentrole'], eol_ext=eol) - # uses = dict(expiry=reagentrole['expiry'], lot=reagentrole['lot'], name=reagentrole['name'], sheet=reagentrole['sheet']) - # ktrr_assoc = KitTypeReagentRoleAssociation(kittype=new_kit, reagentrole=new_role, uses=uses) - # ktrr_assoc.proceduretype = proceduretype - # ktrr_assoc.required = reagentrole['required'] - # ktst_assoc = SubmissionTypeKitTypeAssociation( - # kittype=new_kit, - # proceduretype=proceduretype, - # mutable_cost_sample=import_dict['mutable_cost_sample'], - # mutable_cost_column=import_dict['mutable_cost_column'], - # constant_cost=import_dict['constant_cost'] - # ) - # for reagentrole in import_dict['kittype']['equipmentrole']: - # new_role = EquipmentRole.query(name=reagentrole['reagentrole']) - # if new_role: - # check = input(f"Found existing reagentrole: {new_role.name}. Use this? [Y/n]: ") - # if check.lower() == "n": - # new_role = None - # else: - # pass - # if not new_role: - # new_role = EquipmentRole(name=reagentrole['reagentrole']) - # for equipment in Equipment.assign_equipment(equipmentrole=new_role): - # new_role.control.append(equipment) - # ster_assoc = ProcedureTypeEquipmentRoleAssociation(proceduretype=proceduretype, - # equipmentrole=new_role) - # try: - # uses = dict(name=reagentrole['name'], process=reagentrole['process'], sheet=reagentrole['sheet'], - # static=reagentrole['static']) - # except KeyError: - # uses = None - # ster_assoc.uses = uses - # for process in reagentrole['process']: - # new_process = Process.query(name=process) - # if not new_process: - # new_process = Process(name=process) - # new_process.proceduretype.append(proceduretype) - # new_process.kittype.append(new_kit) - # new_process.equipmentrole.append(new_role) - # return new_kit - - def to_omni(self, expand: bool = False) -> "OmniKitType": - from backend.validators.omni_gui_objects import OmniKitType - if expand: - processes = [item.to_omni() for item in self.process] - kittypereagentroleassociation = [item.to_omni() for item in self.kittypereagentroleassociation] - kittypeproceduretypeassociation = [item.to_omni() for item in self.kittypeproceduretypeassociation] - else: - processes = [item.name for item in self.processes] - kittypereagentroleassociation = [item.name for item in self.kittypereagentroleassociation] - kittypeproceduretypeassociation = [item.name for item in self.kittypeproceduretypeassociation] - data = dict( - name=self.name, - processes=processes, - kit_reagentrole_associations=kittypereagentroleassociation, - kit_submissiontype_associations=kittypeproceduretypeassociation - ) - # logger.debug(f"Creating omni for {pformat(data)}") - return OmniKitType(instance_object=self, **data) +# def to_omni(self, expand: bool = False) -> "OmniKitType": +# from backend.validators.omni_gui_objects import OmniKitType +# if expand: +# processes = [item.to_omni() for item in self.process] +# kittypereagentroleassociation = [item.to_omni() for item in self.kittypereagentroleassociation] +# kittypeproceduretypeassociation = [item.to_omni() for item in self.kittypeproceduretypeassociation] +# else: +# processes = [item.name for item in self.processes] +# kittypereagentroleassociation = [item.name for item in self.kittypereagentroleassociation] +# kittypeproceduretypeassociation = [item.name for item in self.kittypeproceduretypeassociation] +# data = dict( +# name=self.name, +# processes=processes, +# kit_reagentrole_associations=kittypereagentroleassociation, +# kit_submissiontype_associations=kittypeproceduretypeassociation +# ) +# # logger.debug(f"Creating omni for {pformat(data)}") +# return OmniKitType(instance_object=self, **data) class ReagentRole(BaseClass): @@ -455,18 +456,17 @@ class ReagentRole(BaseClass): name = Column(String(64)) #: name of reagentrole reagent plays reagent = relationship("Reagent", back_populates="reagentrole", secondary=reagentrole_reagent) #: concrete control of this reagent type - eol_ext = Column(Interval()) #: extension of life interval - reagentrolekittypeassociation = relationship( - "KitTypeReagentRoleAssociation", + reagentroleproceduretypeassociation = relationship( + "ProcedureTypeReagentRoleAssociation", back_populates="reagentrole", cascade="all, delete-orphan", ) #: Relation to KitTypeReagentTypeAssociation # creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 - kittype = association_proxy("reagentrolekittypeassociation", "kittype", - creator=lambda kit: KitTypeReagentRoleAssociation( - kittype=kit)) #: Association proxy to KitTypeReagentRoleAssociation + proceduretype = association_proxy("reagentroleproceduretypeassociation", "proceduretype", + creator=lambda proceduretype: ProcedureTypeReagentRoleAssociation( + proceduretype=proceduretype)) #: Association proxy to KitTypeReagentRoleAssociation @classmethod def query_or_create(cls, **kwargs) -> Tuple[ReagentRole, bool]: @@ -486,7 +486,7 @@ class ReagentRole(BaseClass): @setup_lookup def query(cls, name: str | None = None, - kittype: KitType | str | None = None, + proceduretype: ProcedureType | str | None = None, reagent: Reagent | str | None = None, id: int | None = None, limit: int = 0, @@ -509,14 +509,14 @@ class ReagentRole(BaseClass): ReagentRole|List[ReagentRole]: ReagentRole or list of ReagentRoles matching filter. """ query: Query = cls.__database_session__.query(cls) - if (kittype is not None and reagent is None) or (reagent is not None and kittype is None): + if (proceduretype is not None and reagent is None) or (reagent is not None and proceduretype is None): raise ValueError("Cannot filter without both reagent and kittype type.") - elif kittype is None and reagent is None: + elif proceduretype is None and reagent is None: pass else: - match kittype: + match proceduretype: case str(): - kittype = KitType.query(name=kittype) + proceduretype = ProcedureType.query(name=proceduretype) case _: pass match reagent: @@ -526,7 +526,7 @@ class ReagentRole(BaseClass): pass assert reagent.role # NOTE: Get all roles common to the reagent and the kittype. - result = set(kittype.reagentrole).intersection(reagent.role) + result = set(proceduretype.reagentrole).intersection(reagent.role) return next((item for item in result), None) match name: case str(): @@ -561,13 +561,14 @@ class ReagentRole(BaseClass): logger.debug(f"Constructing OmniReagentRole with name {self.name}") return OmniReagentRole(instance_object=self, name=self.name, eol_ext=self.eol_ext) - def get_reagents(self, kittype: str | KitType | None = None): - if not kittype: + def get_reagents(self, proceduretype: str | ProcedureType | None = None): + if not proceduretype: # return [f"{reagent.name} - {reagent.lot} - {reagent.expiry}" for reagent in self.reagent] return [reagent.to_pydantic() for reagent in self.reagent] - if isinstance(kittype, str): - kittype = KitType.query(name=kittype) - assoc = next((item for item in self.reagentrolekittypeassociation if item.kittype == kittype), None) + if isinstance(proceduretype, str): + proceduretype = ProcedureType.query(name=proceduretype) + assoc = next((item for item in self.reagentroleproceduretypeassociation if item.proceduretype == proceduretype), + None) reagents = [reagent for reagent in self.reagent] if assoc: last_used = Reagent.query(name=assoc.last_used) @@ -578,6 +579,10 @@ class ReagentRole(BaseClass): # return [f"{reagent.name} - {reagent.lot} - {reagent.expiry}" for reagent in reagents] return [reagent.to_pydantic(reagentrole=self.name) for reagent in reagents] + def details_dict(self, **kwargs): + output = super().details_dict(**kwargs) + return output + class Reagent(BaseClass, LogMixin): """ @@ -589,80 +594,78 @@ class Reagent(BaseClass, LogMixin): secondary=reagentrole_reagent) #: joined parent reagent type reagentrole_id = Column(INTEGER, ForeignKey("_reagentrole.id", ondelete='SET NULL', name="fk_REG_reagent_role_id")) #: id of parent reagent type + eol_ext = Column(Interval()) #: extension of life interval name = Column(String(64)) #: reagent name - lot = Column(String(64)) #: lot number of reagent - expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically - - reagentprocedureassociation = relationship( - "ProcedureReagentAssociation", - back_populates="reagent", - cascade="all, delete-orphan", - ) #: Relation to ClientSubmissionSampleAssociation - - procedures = association_proxy("reagentprocedureassociation", "procedure", - creator=lambda procedure: ProcedureReagentAssociation( - procedure=procedure)) #: Association proxy to ClientSubmissionSampleAssociation.sample + cost_per_ml = Column(FLOAT) + lots = relationship("ReagentLot") def __repr__(self): if self.name: - name = f"" + name = f"" else: - name = f"" + name = f"" return name + def __init__(self, name: str, eol_ext: timedelta = timedelta(0), *args, **kwargs): + super().__init__(*args, **kwargs) + self.name = name + self.eol_ext = eol_ext + # for key, value in kwargs.items(): + # setattr(self, key, value) + @classproperty def searchables(cls): return [dict(label="Lot", field="lot")] - def to_sub_dict(self, kittype: KitType = None, full_data: bool = False, **kwargs) -> dict: - """ - dictionary containing values necessary for gui + # def to_sub_dict(self, kittype: KitType = None, full_data: bool = False, **kwargs) -> dict: + # """ + # dictionary containing values necessary for gui + # + # Args: + # kittype (KitType, optional): KitType to use to get reagent type. Defaults to None. + # full_data (bool, optional): Whether to include procedure in data for details. Defaults to False. + # + # Returns: + # dict: representation of the reagent's attributes + # """ + # if kittype is not None: + # # NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType + # reagent_role = next((item for item in set(self.reagentrole).intersection(kittype.reagentrole)), + # self.reagentrole[0]) + # else: + # try: + # reagent_role = self.reagentrole[0] + # except IndexError: + # reagent_role = None + # try: + # rtype = reagent_role.name.replace("_", " ") + # except AttributeError: + # rtype = "Unknown" + # # NOTE: Calculate expiry with EOL from ReagentType + # try: + # place_holder = self.expiry + reagent_role.eol_ext + # except (TypeError, AttributeError) as e: + # place_holder = date.today() + # logger.error(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing") + # # NOTE: The notation for not having an expiry is 1970.01.01 + # if self.expiry.year == 1970: + # place_holder = "NA" + # else: + # place_holder = place_holder.strftime("%Y-%m-%d") + # output = dict( + # name=self.name, + # reagentrole=rtype, + # lot=self.lot, + # expiry=place_holder, + # missing=False + # ) + # if full_data: + # output['procedure'] = [sub.rsl_plate_number for sub in self.procedures] + # output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] + # output['editable'] = ['lot', 'expiry'] + # return output - Args: - kittype (KitType, optional): KitType to use to get reagent type. Defaults to None. - full_data (bool, optional): Whether to include procedure in data for details. Defaults to False. - - Returns: - dict: representation of the reagent's attributes - """ - if kittype is not None: - # NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType - reagent_role = next((item for item in set(self.reagentrole).intersection(kittype.reagentrole)), - self.reagentrole[0]) - else: - try: - reagent_role = self.reagentrole[0] - except IndexError: - reagent_role = None - try: - rtype = reagent_role.name.replace("_", " ") - except AttributeError: - rtype = "Unknown" - # NOTE: Calculate expiry with EOL from ReagentType - try: - place_holder = self.expiry + reagent_role.eol_ext - except (TypeError, AttributeError) as e: - place_holder = date.today() - logger.error(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing") - # NOTE: The notation for not having an expiry is 1970.01.01 - if self.expiry.year == 1970: - place_holder = "NA" - else: - place_holder = place_holder.strftime("%Y-%m-%d") - output = dict( - name=self.name, - reagentrole=rtype, - lot=self.lot, - expiry=place_holder, - missing=False - ) - if full_data: - output['procedure'] = [sub.rsl_plate_number for sub in self.procedures] - output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] - output['editable'] = ['lot', 'expiry'] - return output - - def update_last_used(self, kit: KitType) -> Report: + def update_last_used(self, proceduretype: ProcedureType) -> Report: """ Updates last used reagent lot for ReagentType/KitType @@ -673,9 +676,9 @@ class Reagent(BaseClass, LogMixin): Report: Result of operation """ report = Report() - rt = ReagentRole.query(kittype=kit, reagent=self, limit=1) + rt = ReagentRole.query(proceduretype=proceduretype, reagent=self, limit=1) if rt is not None: - assoc = KitTypeReagentRoleAssociation.query(kittype=kit, reagentrole=rt) + assoc = ProcedureTypeReagentRoleAssociation.query(proceduretype=proceduretype, reagentrole=rt) if assoc is not None: if assoc.last_used != self.lot: assoc.last_used = self.lot @@ -773,13 +776,13 @@ class Reagent(BaseClass, LogMixin): return case "comment": return - case "expiry": - if isinstance(value, str): - value = date(year=1970, month=1, day=1) - # NOTE: if min time is used, any reagent set to expire today (Bac postive control, eg) will have expired at midnight and therefore be flagged. - # NOTE: Make expiry at date given, plus maximum time = end of day - value = datetime.combine(value, datetime.max.time()) - value = value.replace(tzinfo=timezone) + # case "expiry": + # if isinstance(value, str): + # value = date(year=1970, month=1, day=1) + # # NOTE: if min time is used, any reagent set to expire today (Bac postive control, eg) will have expired at midnight and therefore be flagged. + # # NOTE: Make expiry at date given, plus maximum time = end of day + # value = datetime.combine(value, datetime.max.time()) + # value = value.replace(tzinfo=timezone) case _: pass logger.debug(f"Role to be set to: {value}") @@ -805,7 +808,7 @@ class Reagent(BaseClass, LogMixin): expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically" ) - def details_dict(self, reagentrole:str|None=None, **kwargs): + def details_dict(self, reagentrole: str | None = None, **kwargs): output = super().details_dict() if reagentrole: output['reagentrole'] = reagentrole @@ -813,6 +816,49 @@ class Reagent(BaseClass, LogMixin): output['reagentrole'] = self.reagentrole[0].name return output + @property + def lot_dicts(self): + return [dict(name=self.name, lot=lot.lot, expiry=lot.expiry + self.eol_ext) for lot in self.lots] + + +class ReagentLot(BaseClass): + id = Column(INTEGER, primary_key=True) #: primary key + lot = Column(String(64)) #: lot number of reagent + expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically + reagent = relationship("Reagent") #: joined parent reagent type + reagent_id = Column(INTEGER, ForeignKey("_reagent.id", ondelete='SET NULL', + name="fk_REGLOT_reagent_id")) #: id of parent reagent type + + reagentlotprocedureassociation = relationship( + "ProcedureReagentLotAssociation", + back_populates="reagentlot", + cascade="all, delete-orphan", + ) #: Relation to ClientSubmissionSampleAssociation + + procedures = association_proxy("reagentlotprocedureassociation", "procedure", + creator=lambda procedure: ProcedureReagentLotAssociation( + procedure=procedure)) #: Association proxy to ClientSubmissionSampleAssociation.sample + + @hybrid_property + def name(self): + return self.lot + + def __repr__(self): + return f"" + + def set_attribute(self, key, value): + match key: + case "expiry": + if isinstance(value, str): + value = date(year=1970, month=1, day=1) + # NOTE: if min time is used, any reagent set to expire today (Bac postive control, eg) will have expired at midnight and therefore be flagged. + # NOTE: Make expiry at date given, plus maximum time = end of day + value = datetime.combine(value, datetime.max.time()) + value = value.replace(tzinfo=timezone) + case _: + pass + setattr(self, key, value) + class Discount(BaseClass): """ @@ -822,9 +868,9 @@ class Discount(BaseClass): skip_on_edit = True id = Column(INTEGER, primary_key=True) #: primary key - kittype = relationship("KitType") #: joined parent reagent type - kittype_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete='SET NULL', - name="fk_DIS_kit_type_id")) #: id of joined kittype + proceduretype = relationship("ProcedureType") #: joined parent reagent type + proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete='SET NULL', + name="fk_DIS_procedure_type_id")) #: id of joined kittype clientlab = relationship("ClientLab") #: joined client lab clientlab_id = Column(INTEGER, ForeignKey("_clientlab.id", ondelete='SET NULL', @@ -888,12 +934,12 @@ class SubmissionType(BaseClass): id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(128), unique=True) #: name of procedure type - info_map = Column(JSON) #: Where parsable information is found in the excel workbook corresponding to this type. + # info_map = Column(JSON) #: Where parsable information is found in the excel workbook corresponding to this type. defaults = Column(JSON) #: Basic information about this procedure type clientsubmission = relationship("ClientSubmission", back_populates="submissiontype") #: Concrete control of this type. - template_file = Column(BLOB) #: Blank form for this type stored as binary. - sample_map = Column(JSON) #: Where sample information is found in the excel sheet corresponding to this type. + # template_file = Column(BLOB) #: Blank form for this type stored as binary. + # sample_map = Column(JSON) #: Where sample information is found in the excel sheet corresponding to this type. proceduretype = relationship("ProcedureType", back_populates="submissiontype", secondary=submissiontype_proceduretype) #: run this kittype was used for @@ -914,79 +960,79 @@ class SubmissionType(BaseClass): """ return super().aliases + ["submissiontypes"] - @classproperty - def omni_removes(cls): - return super().omni_removes + ["defaults"] + # @classproperty + # def omni_removes(cls): + # return super().omni_removes + ["defaults"] + # + # @classproperty + # def basic_template(cls) -> bytes: + # """ + # Grabs the default excel template file. + # + # Returns: + # bytes: The Excel sheet. + # """ + # submission_type = cls.query(name="Bacterial Culture") + # return submission_type.template_file + # + # @property + # def template_file_sheets(self) -> List[str]: + # """ + # Gets names of sheet in the stored blank form. + # + # Returns: + # List[str]: List of sheet names + # """ + # try: + # return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names + # except zipfile.BadZipfile: + # return [] - @classproperty - def basic_template(cls) -> bytes: - """ - Grabs the default excel template file. - - Returns: - bytes: The Excel sheet. - """ - submission_type = cls.query(name="Bacterial Culture") - return submission_type.template_file - - @property - def template_file_sheets(self) -> List[str]: - """ - Gets names of sheet in the stored blank form. - - Returns: - List[str]: List of sheet names - """ - try: - return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names - except zipfile.BadZipfile: - return [] - - def set_template_file(self, filepath: Path | str): - """ - - Sets the binary store to an Excel file. - - Args: - filepath (Path | str): Path to the template file. - - Raises: - ValueError: Raised if file is not Excel file. - """ - if isinstance(filepath, str): - filepath = Path(filepath) - try: - ExcelFile(filepath) - except ValueError: - raise ValueError(f"File {filepath} is not of appropriate type.") - with open(filepath, "rb") as f: - data = f.read() - self.template_file = data - self.save() - - def construct_info_map(self, mode: Literal['read', 'write', 'export']) -> dict: - """ - Make of map of where all fields are located in Excel sheet - - Args: - mode (Literal["read", "write"]): Which mode to get locations for - - Returns: - dict: Map of locations - """ - info = {k: v for k, v in self.info_map.items() if k != "custom"} - match mode: - case "read": - output = {k: v[mode] for k, v in info.items() if v[mode]} - case "write": - output = {k: v[mode] + v['read'] for k, v in info.items() if v[mode] or v['read']} - output = {k: v for k, v in output.items() if all([isinstance(item, dict) for item in v])} - case "export": - return self.info_map - case _: - output = {} - output['custom'] = self.info_map['custom'] - return output + # def set_template_file(self, filepath: Path | str): + # """ + # + # Sets the binary store to an Excel file. + # + # Args: + # filepath (Path | str): Path to the template file. + # + # Raises: + # ValueError: Raised if file is not Excel file. + # """ + # if isinstance(filepath, str): + # filepath = Path(filepath) + # try: + # ExcelFile(filepath) + # except ValueError: + # raise ValueError(f"File {filepath} is not of appropriate type.") + # with open(filepath, "rb") as f: + # data = f.read() + # self.template_file = data + # self.save() + # + # def construct_info_map(self, mode: Literal['read', 'write', 'export']) -> dict: + # """ + # Make of map of where all fields are located in Excel sheet + # + # Args: + # mode (Literal["read", "write"]): Which mode to get locations for + # + # Returns: + # dict: Map of locations + # """ + # info = {k: v for k, v in self.info_map.items() if k != "custom"} + # match mode: + # case "read": + # output = {k: v[mode] for k, v in info.items() if v[mode]} + # case "write": + # output = {k: v[mode] + v['read'] for k, v in info.items() if v[mode] or v['read']} + # output = {k: v for k, v in output.items() if all([isinstance(item, dict) for item in v])} + # case "export": + # return self.info_map + # case _: + # output = {} + # output['custom'] = self.info_map['custom'] + # return output def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]: """ @@ -1129,14 +1175,15 @@ class SubmissionType(BaseClass): class ProcedureType(BaseClass): id = Column(INTEGER, primary_key=True) name = Column(String(64)) - reagent_map = Column(JSON) - info_map = Column(JSON) - sample_map = Column(JSON) - equipment_map = Column(JSON) + # reagent_map = Column(JSON) + # info_map = Column(JSON) + # sample_map = Column(JSON) + # equipment_map = Column(JSON) plate_columns = Column(INTEGER, default=0) plate_rows = Column(INTEGER, default=0) allowed_result_methods = Column(JSON) - template_file = Column(BLOB) + # template_file = Column(BLOB) + plate_cost = Column(FLOAT) procedure = relationship("Procedure", back_populates="proceduretype") #: Concrete control of this type. @@ -1147,15 +1194,15 @@ class ProcedureType(BaseClass): submissiontype = relationship("SubmissionType", back_populates="proceduretype", secondary=submissiontype_proceduretype) #: run this kittype was used for - proceduretypekittypeassociation = relationship( - "ProcedureTypeKitTypeAssociation", - back_populates="proceduretype", - cascade="all, delete-orphan", - ) #: Association of kittypes - - kittype = association_proxy("proceduretypekittypeassociation", "kittype", - creator=lambda kit: ProcedureTypeKitTypeAssociation( - kittype=kit)) #: Proxy of kittype association + # proceduretypekittypeassociation = relationship( + # "ProcedureTypeKitTypeAssociation", + # back_populates="proceduretype", + # cascade="all, delete-orphan", + # ) #: Association of kittypes + # + # kittype = association_proxy("proceduretypekittypeassociation", "kittype", + # creator=lambda kit: ProcedureTypeKitTypeAssociation( + # kittype=kit)) #: Proxy of kittype association proceduretypeequipmentroleassociation = relationship( "ProcedureTypeEquipmentRoleAssociation", @@ -1163,16 +1210,20 @@ class ProcedureType(BaseClass): cascade="all, delete-orphan" ) #: Association of equipmentroles - equipment = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole", - creator=lambda eq: ProcedureTypeEquipmentRoleAssociation( - equipmentrole=eq)) #: Proxy of equipmentrole associations + equipmentrole = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole", + creator=lambda eq: ProcedureTypeEquipmentRoleAssociation( + equipmentrole=eq)) #: Proxy of equipmentrole associations - kittypereagentroleassociation = relationship( - "KitTypeReagentRoleAssociation", + proceduretypereagentroleassociation = relationship( + "ProcedureTypeReagentRoleAssociation", back_populates="proceduretype", cascade="all, delete-orphan" ) #: triple association of KitTypes, ReagentTypes, SubmissionTypes + reagentrole = association_proxy("proceduretypereagentroleassociation", "reagentrole", + creator=lambda reagentrole: ProcedureTypeReagentRoleAssociation( + reagentrole=reagentrole)) #: Proxy of equipmentrole associations + proceduretypetiproleassociation = relationship( "ProcedureTypeTipRoleAssociation", back_populates="proceduretype", @@ -1183,41 +1234,40 @@ class ProcedureType(BaseClass): super().__init__(*args, **kwargs) self.allowed_result_methods = dict() - @property - def template_file_sheets(self) -> List[str]: - """ - Gets names of sheet in the stored blank form. - - Returns: - List[str]: List of sheet names - """ - try: - return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names - except zipfile.BadZipfile: - return [] - - def set_template_file(self, filepath: Path | str): - """ - - Sets the binary store to an Excel file. - - Args: - filepath (Path | str): Path to the template file. - - Raises: - ValueError: Raised if file is not Excel file. - """ - if isinstance(filepath, str): - filepath = Path(filepath) - try: - ExcelFile(filepath) - except ValueError: - raise ValueError(f"File {filepath} is not of appropriate type.") - with open(filepath, "rb") as f: - data = f.read() - self.template_file = data - self.save() - + # @property + # def template_file_sheets(self) -> List[str]: + # """ + # Gets names of sheet in the stored blank form. + # + # Returns: + # List[str]: List of sheet names + # """ + # try: + # return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names + # except zipfile.BadZipfile: + # return [] + # + # def set_template_file(self, filepath: Path | str): + # """ + # + # Sets the binary store to an Excel file. + # + # Args: + # filepath (Path | str): Path to the template file. + # + # Raises: + # ValueError: Raised if file is not Excel file. + # """ + # if isinstance(filepath, str): + # filepath = Path(filepath) + # try: + # ExcelFile(filepath) + # except ValueError: + # raise ValueError(f"File {filepath} is not of appropriate type.") + # with open(filepath, "rb") as f: + # data = f.read() + # self.template_file = data + # self.save() def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]: """ @@ -1235,18 +1285,18 @@ class ProcedureType(BaseClass): fmap = {} yield getattr(item, f"{field}_role").name, fmap - @property - def default_kit(self) -> KitType | None: - """ - If only one kits exists for this Submission Type, return it. - - Returns: - KitType | None: - """ - if len(self.kittype) == 1: - return self.kittype[0] - else: - return None + # @property + # def default_kit(self) -> KitType | None: + # """ + # If only one kits exists for this Submission Type, return it. + # + # Returns: + # KitType | None: + # """ + # if len(self.kittype) == 1: + # return self.kittype[0] + # else: + # return None def get_equipment(self, kittype: str | KitType | None = None) -> Generator['PydEquipmentRole', None, None]: """ @@ -1282,20 +1332,19 @@ class ProcedureType(BaseClass): raise TypeError(f"Type {type(equipmentrole)} is not allowed") return list(set([item for items in relevant for item in items if item is not None])) - @property - def as_dict(self): - return dict( - name=self.name, - kittype=[item.name for item in self.kittype], - plate_rows=self.plate_rows, - plate_columns=self.plate_columns - ) + # @property + # def as_dict(self): + # return dict( + # name=self.name, + # kittype=[item.name for item in self.kittype], + # plate_rows=self.plate_rows, + # plate_columns=self.plate_columns + # ) def details_dict(self, **kwargs): output = super().details_dict(**kwargs) - output['kittype'] = [item.details_dict() for item in output['kittype']] - # output['process'] = [item.details_dict() for item in output['process']] - output['equipment'] = [item.details_dict(proceduretype=self) for item in output['equipment']] + output['reagentrole'] = [item.details_dict() for item in output['reagentrole']] + output['equipment'] = [item.details_dict(proceduretype=self) for item in output['equipmentrole']] return output def construct_dummy_procedure(self, run: Run | None = None): @@ -1350,7 +1399,6 @@ class ProcedureType(BaseClass): # logger.debug(f"Appending {sample} at row {row}, column {column}") return output - @property def ranked_plate(self): matrix = np.array([[0 for yyy in range(1, self.plate_rows + 1)] for xxx in range(1, self.plate_columns + 1)]) @@ -1377,9 +1425,9 @@ class Procedure(BaseClass): run_id = Column(INTEGER, ForeignKey("_run.id", ondelete="SET NULL", name="fk_PRO_basicrun_id")) #: client lab id from _organizations)) run = relationship("Run", back_populates="procedure") - kittype_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete="SET NULL", - name="fk_PRO_kittype_id")) #: client lab id from _organizations)) - kittype = relationship("KitType", back_populates="procedure") + # kittype_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete="SET NULL", + # name="fk_PRO_kittype_id")) #: client lab id from _organizations)) + # kittype = relationship("KitType", back_populates="procedure") control = relationship("Control", back_populates="procedure", uselist=True) #: A control sample added to procedure proceduresampleassociation = relationship( @@ -1392,14 +1440,14 @@ class Procedure(BaseClass): "sample", creator=lambda sample: ProcedureSampleAssociation(sample=sample) ) - procedurereagentassociation = relationship( - "ProcedureReagentAssociation", + procedurereagentlotassociation = relationship( + "ProcedureReagentLotAssociation", back_populates="procedure", cascade="all, delete-orphan", ) #: Relation to ProcedureReagentAssociation - reagent = association_proxy("procedurereagentassociation", - "reagent", creator=lambda reg: ProcedureReagentAssociation( + reagentlot = association_proxy("procedurereagentlotassociation", + "reagent", creator=lambda reg: ProcedureReagentLotAssociation( reagent=reg)) #: Association proxy to RunReagentAssociation.reagent procedureequipmentassociation = relationship( @@ -1419,8 +1467,6 @@ class Procedure(BaseClass): tips = association_proxy("proceduretipsassociation", "tips") - - @validates('repeat') def validate_repeat(self, key, value): if value > 1: @@ -1448,10 +1494,10 @@ class Procedure(BaseClass): pass return cls.execute_query(query=query, limit=limit) - def to_dict(self, full_data: bool = False): - output = dict() - output['name'] = self.name - return output + # def to_dict(self, full_data: bool = False): + # output = dict() + # output['name'] = self.name + # return output @property def custom_context_events(self) -> dict: @@ -1546,14 +1592,15 @@ class Procedure(BaseClass): output['sample'] = active_samples + inactive_samples logger.debug(f"Procedure samples: \n\n{pformat(output['sample'])}\n\n") # output['sample'] = [sample.details_dict() for sample in output['sample']] - output['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentassociation']] + output['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentlotassociation']] output['equipment'] = [equipment.details_dict() for equipment in output['procedureequipmentassociation']] output['tips'] = [tips.details_dict() for tips in output['proceduretipsassociation']] output['repeat'] = bool(output['repeat']) output['run'] = self.run.name - output['excluded'] += ['id', "results", "proceduresampleassociation", "sample", "procedurereagentassociation", - "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", - "tips", "control", "kittype"] + output['excluded'] += ['id', "results", "proceduresampleassociation", "sample", + "procedurereagentlotassociation", + "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", + "tips", "control", "kittype"] # output = self.clean_details_dict(output) return output @@ -1593,7 +1640,8 @@ class Procedure(BaseClass): # pass # output.results = results output.result = [item.to_pydantic() for item in self.results] - output.sample_results = flatten_list([[result.to_pydantic() for result in item.results] for item in self.proceduresampleassociation]) + output.sample_results = flatten_list( + [[result.to_pydantic() for result in item.results] for item in self.proceduresampleassociation]) # for sample in output.sample: # sample.enabled = True return output @@ -1603,137 +1651,137 @@ class Procedure(BaseClass): return ProcedureSampleAssociation(procedure=self, sample=sample) -class ProcedureTypeKitTypeAssociation(BaseClass): - """ - Abstract of relationship between kits and their procedure type. - """ +# class ProcedureTypeKitTypeAssociation(BaseClass): +# """ +# Abstract of relationship between kits and their procedure type. +# """ +# +# omni_removes = BaseClass.omni_removes + ["proceduretype_id", "kittype_id"] +# omni_sort = ["proceduretype", "kittype"] +# level = 2 +# +# proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), +# primary_key=True) #: id of joined procedure type +# kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kittype +# 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 sample (reagents, tips, etc) +# constant_cost = Column(FLOAT(2)) #: dollar amount per plate that will remain constant (plates, man hours, etc) +# +# kittype = relationship(KitType, back_populates="kittypeproceduretypeassociation") #: joined kittype +# +# # reference to the "SubmissionType" object +# proceduretype = relationship(ProcedureType, +# back_populates="proceduretypekittypeassociation") #: joined procedure type +# +# def __init__(self, kittype=None, proceduretype=None, +# mutable_cost_column: int = 0.00, mutable_cost_sample: int = 0.00, constant_cost: int = 0.00): +# self.kittype = kittype +# self.proceduretype = proceduretype +# self.mutable_cost_column = mutable_cost_column +# self.mutable_cost_sample = mutable_cost_sample +# self.constant_cost = constant_cost +# +# def __repr__(self) -> str: +# """ +# Returns: +# str: Representation of this object +# """ +# try: +# proceduretype_name = self.proceduretype.name +# except AttributeError: +# proceduretype_name = "None" +# try: +# kittype_name = self.kittype.name +# except AttributeError: +# kittype_name = "None" +# return f"" +# +# @property +# def name(self): +# try: +# return f"{self.proceduretype.name} -> {self.kittype.name}" +# except AttributeError: +# return "Blank SubmissionTypeKitTypeAssociation" +# +# @classmethod +# def query_or_create(cls, **kwargs) -> Tuple[ProcedureTypeKitTypeAssociation, bool]: +# new = False +# disallowed = ['expiry'] +# sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} +# instance = cls.query(**sanitized_kwargs) +# if not instance or isinstance(instance, list): +# instance = cls() +# new = True +# for k, v in sanitized_kwargs.items(): +# setattr(instance, k, v) +# logger.info(f"Instance from ProcedureTypeKitTypeAssociation query or create: {instance}") +# return instance, new +# +# @classmethod +# @setup_lookup +# def query(cls, +# proceduretype: ProcedureType | str | int | None = None, +# kittype: KitType | str | int | None = None, +# limit: int = 0, +# **kwargs +# ) -> ProcedureTypeKitTypeAssociation | List[ProcedureTypeKitTypeAssociation]: +# """ +# Lookup SubmissionTypeKitTypeAssociations of interest. +# +# Args: +# proceduretype (ProcedureType | str | int | None, optional): Identifier of procedure type. Defaults to None. +# kittype (KitType | str | int | None, optional): Identifier of kittype type. Defaults to None. +# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. +# +# Returns: +# SubmissionTypeKitTypeAssociation|List[SubmissionTypeKitTypeAssociation]: SubmissionTypeKitTypeAssociation(s) of interest +# """ +# query: Query = cls.__database_session__.query(cls) +# match proceduretype: +# case ProcedureType(): +# query = query.filter(cls.proceduretype == proceduretype) +# case str(): +# query = query.join(ProcedureType).filter(ProcedureType.name == proceduretype) +# case int(): +# query = query.join(ProcedureType).filter(ProcedureType.id == proceduretype) +# match kittype: +# case KitType(): +# query = query.filter(cls.kittype == kittype) +# case str(): +# query = query.join(KitType).filter(KitType.name == kittype) +# case int(): +# query = query.join(KitType).filter(KitType.id == kittype) +# if kittype is not None and proceduretype is not None: +# limit = 1 +# return cls.execute_query(query=query, limit=limit) +# +# def to_omni(self, expand: bool = False): +# from backend.validators.omni_gui_objects import OmniSubmissionTypeKitTypeAssociation +# if expand: +# try: +# submissiontype = self.submission_type.to_omni() +# except AttributeError: +# submissiontype = "" +# try: +# kittype = self.kit_type.to_omni() +# except AttributeError: +# kittype = "" +# else: +# submissiontype = self.submission_type.name +# kittype = self.kit_type.name +# return OmniSubmissionTypeKitTypeAssociation( +# instance_object=self, +# submissiontype=submissiontype, +# kittype=kittype, +# mutable_cost_column=self.mutable_cost_column, +# mutable_cost_sample=self.mutable_cost_sample, +# constant_cost=self.constant_cost +# ) +# - omni_removes = BaseClass.omni_removes + ["proceduretype_id", "kittype_id"] - omni_sort = ["proceduretype", "kittype"] - level = 2 - - proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), - primary_key=True) #: id of joined procedure type - kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kittype - 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 sample (reagents, tips, etc) - constant_cost = Column(FLOAT(2)) #: dollar amount per plate that will remain constant (plates, man hours, etc) - - kittype = relationship(KitType, back_populates="kittypeproceduretypeassociation") #: joined kittype - - # reference to the "SubmissionType" object - proceduretype = relationship(ProcedureType, - back_populates="proceduretypekittypeassociation") #: joined procedure type - - def __init__(self, kittype=None, proceduretype=None, - mutable_cost_column: int = 0.00, mutable_cost_sample: int = 0.00, constant_cost: int = 0.00): - self.kittype = kittype - self.proceduretype = proceduretype - self.mutable_cost_column = mutable_cost_column - self.mutable_cost_sample = mutable_cost_sample - self.constant_cost = constant_cost - - def __repr__(self) -> str: - """ - Returns: - str: Representation of this object - """ - try: - proceduretype_name = self.proceduretype.name - except AttributeError: - proceduretype_name = "None" - try: - kittype_name = self.kittype.name - except AttributeError: - kittype_name = "None" - return f"" - - @property - def name(self): - try: - return f"{self.proceduretype.name} -> {self.kittype.name}" - except AttributeError: - return "Blank SubmissionTypeKitTypeAssociation" - - @classmethod - def query_or_create(cls, **kwargs) -> Tuple[ProcedureTypeKitTypeAssociation, bool]: - new = False - disallowed = ['expiry'] - sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} - instance = cls.query(**sanitized_kwargs) - if not instance or isinstance(instance, list): - instance = cls() - new = True - for k, v in sanitized_kwargs.items(): - setattr(instance, k, v) - logger.info(f"Instance from ProcedureTypeKitTypeAssociation query or create: {instance}") - return instance, new - - @classmethod - @setup_lookup - def query(cls, - proceduretype: ProcedureType | str | int | None = None, - kittype: KitType | str | int | None = None, - limit: int = 0, - **kwargs - ) -> ProcedureTypeKitTypeAssociation | List[ProcedureTypeKitTypeAssociation]: - """ - Lookup SubmissionTypeKitTypeAssociations of interest. - - Args: - proceduretype (ProcedureType | str | int | None, optional): Identifier of procedure type. Defaults to None. - kittype (KitType | str | int | None, optional): Identifier of kittype type. Defaults to None. - limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. - - Returns: - SubmissionTypeKitTypeAssociation|List[SubmissionTypeKitTypeAssociation]: SubmissionTypeKitTypeAssociation(s) of interest - """ - query: Query = cls.__database_session__.query(cls) - match proceduretype: - case ProcedureType(): - query = query.filter(cls.proceduretype == proceduretype) - case str(): - query = query.join(ProcedureType).filter(ProcedureType.name == proceduretype) - case int(): - query = query.join(ProcedureType).filter(ProcedureType.id == proceduretype) - match kittype: - case KitType(): - query = query.filter(cls.kittype == kittype) - case str(): - query = query.join(KitType).filter(KitType.name == kittype) - case int(): - query = query.join(KitType).filter(KitType.id == kittype) - if kittype is not None and proceduretype is not None: - limit = 1 - return cls.execute_query(query=query, limit=limit) - - def to_omni(self, expand: bool = False): - from backend.validators.omni_gui_objects import OmniSubmissionTypeKitTypeAssociation - if expand: - try: - submissiontype = self.submission_type.to_omni() - except AttributeError: - submissiontype = "" - try: - kittype = self.kit_type.to_omni() - except AttributeError: - kittype = "" - else: - submissiontype = self.submission_type.name - kittype = self.kit_type.name - return OmniSubmissionTypeKitTypeAssociation( - instance_object=self, - submissiontype=submissiontype, - kittype=kittype, - mutable_cost_column=self.mutable_cost_column, - mutable_cost_sample=self.mutable_cost_sample, - constant_cost=self.constant_cost - ) - - -class KitTypeReagentRoleAssociation(BaseClass): +class ProcedureTypeReagentRoleAssociation(BaseClass): """ table containing reagenttype/kittype associations DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html @@ -1745,36 +1793,38 @@ class KitTypeReagentRoleAssociation(BaseClass): reagentrole_id = Column(INTEGER, ForeignKey("_reagentrole.id"), primary_key=True) #: id of associated reagent type - kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type + # kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), primary_key=True) uses = Column(JSON) #: map to location on excel sheets of different procedure types required = Column(INTEGER) #: whether the reagent type is required for the kittype (Boolean 1 or 0) last_used = Column(String(32)) #: last used lot number of this type of reagent + ml_used_per_sample = Column(FLOAT) - kittype = relationship(KitType, - back_populates="kittypereagentroleassociation") #: relationship to associated KitType + # kittype = relationship(KitType, + # back_populates="kittypereagentroleassociation") #: relationship to associated KitType # NOTE: reference to the "ReagentType" object reagentrole = relationship(ReagentRole, - back_populates="reagentrolekittypeassociation") #: relationship to associated ReagentType + back_populates="reagentroleproceduretypeassociation") #: relationship to associated ReagentType # NOTE: reference to the "SubmissionType" object proceduretype = relationship(ProcedureType, - back_populates="kittypereagentroleassociation") #: relationship to associated SubmissionType + back_populates="proceduretypereagentroleassociation") #: relationship to associated SubmissionType - def __init__(self, kittype=None, reagentrole=None, uses=None, required=1): - self.kittype = kittype + def __init__(self, proceduretype=None, reagentrole=None, uses=None, required=1): + # self.kittype = kittype + self.proceduretype = proceduretype self.reagentrole = reagentrole self.uses = uses self.required = required def __repr__(self) -> str: - return f"" + return f"" @property def name(self): try: - return f"{self.kittype.name} -> {self.reagentrole.name}" + return f"{self.proceduretype.name} -> {self.reagentrole.name}" except AttributeError: return "Blank KitTypeReagentRole" @@ -1819,7 +1869,7 @@ class KitTypeReagentRoleAssociation(BaseClass): return value @classmethod - def query_or_create(cls, **kwargs) -> Tuple[KitTypeReagentRoleAssociation, bool]: + def query_or_create(cls, **kwargs) -> Tuple[ProcedureTypeReagentRoleAssociation, bool]: new = False disallowed = ['expiry'] sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} @@ -1830,11 +1880,11 @@ class KitTypeReagentRoleAssociation(BaseClass): for k, v in sanitized_kwargs.items(): logger.debug(f"Key: {k} has value: {v}") match k: - case "kittype": - if isinstance(v, str): - v = KitType.query(name=v) - else: - v = v.instance_object + # case "kittype": + # if isinstance(v, str): + # v = KitType.query(name=v) + # else: + # v = v.instance_object case "proceduretype": if isinstance(v, str): v = SubmissionType.query(name=v) @@ -1854,12 +1904,12 @@ class KitTypeReagentRoleAssociation(BaseClass): @classmethod @setup_lookup def query(cls, - kittype: KitType | str | None = None, + # kittype: KitType | str | None = None, reagentrole: ReagentRole | str | None = None, proceduretype: ProcedureType | str | None = None, limit: int = 0, **kwargs - ) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]: + ) -> ProcedureTypeReagentRoleAssociation | List[ProcedureTypeReagentRoleAssociation]: """ Lookup junction of ReagentType and KitType @@ -1872,13 +1922,13 @@ class KitTypeReagentRoleAssociation(BaseClass): models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest. """ query: Query = cls.__database_session__.query(cls) - match kittype: - case KitType(): - query = query.filter(cls.kit_type == kittype) - case str(): - query = query.join(KitType).filter(KitType.name == kittype) - case _: - pass + # match kittype: + # case KitType(): + # query = query.filter(cls.kit_type == kittype) + # case str(): + # query = query.join(KitType).filter(KitType.name == kittype) + # case _: + # pass match reagentrole: case ReagentRole(): query = query.filter(cls.reagent_role == reagentrole) @@ -1894,8 +1944,8 @@ class KitTypeReagentRoleAssociation(BaseClass): case _: pass pass - if kittype is not None and reagentrole is not None: - limit = 1 + # if kittype is not None and reagentrole is not None: + # limit = 1 return cls.execute_query(query=query, limit=limit) def get_all_relevant_reagents(self) -> Generator[Reagent, None, None]: @@ -1965,7 +2015,7 @@ class KitTypeReagentRoleAssociation(BaseClass): ) -class ProcedureReagentAssociation(BaseClass): +class ProcedureReagentLotAssociation(BaseClass): """ table containing procedure/reagent associations DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html @@ -1973,15 +2023,15 @@ class ProcedureReagentAssociation(BaseClass): skip_on_edit = True - reagent_id = Column(INTEGER, ForeignKey("_reagent.id"), primary_key=True) #: id of associated reagent + reagentlot_id = Column(INTEGER, ForeignKey("_reagentlot.id"), primary_key=True) #: id of associated reagent procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure reagentrole = Column(String(64)) comments = Column(String(1024)) #: Comments about reagents procedure = relationship("Procedure", - back_populates="procedurereagentassociation") #: associated procedure + back_populates="procedurereagentlotassociation") #: associated procedure - reagent = relationship(Reagent, back_populates="reagentprocedureassociation") #: associated reagent + reagentlot = relationship(ReagentLot, back_populates="reagentlotprocedureassociation") #: associated reagent def __repr__(self) -> str: """ @@ -1997,11 +2047,11 @@ class ProcedureReagentAssociation(BaseClass): return "" return f"" - def __init__(self, reagent=None, procedure=None, reagentrole=""): - if isinstance(reagent, list): - logger.warning(f"Got list for reagent. Likely no lot was provided. Using {reagent[0]}") - reagent = reagent[0] - self.reagent = reagent + def __init__(self, reagentlot=None, procedure=None, reagentrole=""): + if isinstance(reagentlot, list): + logger.warning(f"Got list for reagent. Likely no lot was provided. Using {reagentlot[0]}") + reagentlot = reagentlot[0] + self.reagentlot = reagentlot self.procedure = procedure self.reagentrole = reagentrole self.comments = "" @@ -2010,9 +2060,9 @@ class ProcedureReagentAssociation(BaseClass): @setup_lookup def query(cls, procedure: Procedure | str | int | None = None, - reagent: Reagent | str | None = None, + reagentlot: Reagent | str | None = None, reagentrole: str | None = None, - limit: int = 0) -> ProcedureReagentAssociation | List[ProcedureReagentAssociation]: + limit: int = 0) -> ProcedureReagentLotAssociation | List[ProcedureReagentLotAssociation]: """ Lookup SubmissionReagentAssociations of interest. @@ -2043,26 +2093,26 @@ class ProcedureReagentAssociation(BaseClass): case _: pass if reagentrole: - query = query.filter(cls.reagentrole==reagentrole) + query = query.filter(cls.reagentrole == reagentrole) return cls.execute_query(query=query, limit=limit) - def to_sub_dict(self, kittype) -> dict: - """ - Converts this RunReagentAssociation (and associated Reagent) to dict + # def to_sub_dict(self, kittype) -> dict: + # """ + # Converts this RunReagentAssociation (and associated Reagent) to dict + # + # Args: + # kittype (_type_): Extraction kittype of interest + # + # Returns: + # dict: This RunReagentAssociation as dict + # """ + # output = self.reagent.to_sub_dict(kittype) + # output['comments'] = self.comments + # return output - Args: - kittype (_type_): Extraction kittype of interest - - Returns: - dict: This RunReagentAssociation as dict - """ - output = self.reagent.to_sub_dict(kittype) - output['comments'] = self.comments - return output - - def to_pydantic(self, kittype: KitType): + def to_pydantic(self): from backend.validators import PydReagent - return PydReagent(**self.to_sub_dict(kittype=kittype)) + return PydReagent(**self.details_dict()) def details_dict(self, **kwargs): output = super().details_dict() @@ -2110,6 +2160,14 @@ class Equipment(BaseClass, LogMixin): procedure = association_proxy("equipmentprocedureassociation", "procedure") #: proxy to equipmentprocedureassociation.procedure + def __init__(self, name: str, nickname: str | None = None, asset_number: str = ""): + self.name = name + if nickname: + self.nickname = nickname + else: + self.nickname = self.name + self.asset_number = asset_number + def to_dict(self, processes: bool = False) -> dict: """ This Equipment as a dictionary @@ -2126,7 +2184,7 @@ class Equipment(BaseClass, LogMixin): return {k: v for k, v in self.__dict__.items()} def get_processes(self, proceduretype: str | ProcedureType | None = None, - kittype: str | KitType | None = None, + # kittype: str | KitType | None = None, equipmentrole: str | EquipmentRole | None = None) -> Generator[Process, None, None]: """ Get all process associated with this Equipment for a given SubmissionType @@ -2140,15 +2198,12 @@ class Equipment(BaseClass, LogMixin): """ if isinstance(proceduretype, str): proceduretype = ProcedureType.query(name=proceduretype) - if isinstance(kittype, str): - kittype = KitType.query(name=kittype) for process in self.process: if proceduretype not in process.proceduretype: continue - if kittype and kittype not in process.kittype: - continue if equipmentrole and equipmentrole not in process.equipmentrole: continue + logger.debug(f"Getting process: {process}") yield process @classmethod @@ -2199,7 +2254,7 @@ class Equipment(BaseClass, LogMixin): pass return cls.execute_query(query=query, limit=limit) - def to_pydantic(self, proceduretype: ProcedureType, kittype: str | KitType | None = None, + def to_pydantic(self, proceduretype: ProcedureType, equipmentrole: str = None) -> "PydEquipment": """ Creates PydEquipment of this Equipment @@ -2213,8 +2268,7 @@ class Equipment(BaseClass, LogMixin): """ from backend.validators.pydant import PydEquipment creation_dict = self.details_dict() - processes = self.get_processes(proceduretype=proceduretype, kittype=kittype, - equipmentrole=equipmentrole) + processes = self.get_processes(proceduretype=proceduretype, equipmentrole=equipmentrole) logger.debug(f"Processes: {processes}") creation_dict['processes'] = processes logger.debug(f"EquipmentRole: {equipmentrole}") @@ -2264,33 +2318,42 @@ class Equipment(BaseClass, LogMixin): output.append(equipment[choice]) return output - def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict: - """ - dictionary containing values necessary for gui + # def details_dict(self, **kwargs): + # output = super().details_dict(**kwargs) + # for key in ["proceduretype", "equipmentroleproceduretypeassociation", "equipmentrole"]: + # try: + # del output[key] + # except KeyError: + # pass + # return output - Args: - full_data (bool, optional): Whether to include procedure in data for details. Defaults to False. - - Returns: - dict: representation of the equipment's attributes - """ - if self.nickname: - nickname = self.nickname - else: - nickname = self.name - output = dict( - name=self.name, - nickname=nickname, - asset_number=self.asset_number - ) - if full_data: - subs = [dict(plate=item.procedure.procedure.rsl_plate_number, process=item.process.name, - sub_date=item.procedure.procedure.start_date) - if item.process else dict(plate=item.procedure.procedure.rsl_plate_number, process="NA") - for item in self.equipmentprocedureassociation] - output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True) - output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] - return output + # def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict: + # """ + # dictionary containing values necessary for gui + # + # Args: + # full_data (bool, optional): Whether to include procedure in data for details. Defaults to False. + # + # Returns: + # dict: representation of the equipment's attributes + # """ + # if self.nickname: + # nickname = self.nickname + # else: + # nickname = self.name + # output = dict( + # name=self.name, + # nickname=nickname, + # asset_number=self.asset_number + # ) + # if full_data: + # subs = [dict(plate=item.procedure.procedure.rsl_plate_number, process=item.process.name, + # sub_date=item.procedure.procedure.start_date) + # if item.process else dict(plate=item.procedure.procedure.rsl_plate_number, process="NA") + # for item in self.equipmentprocedureassociation] + # output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True) + # output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] + # return output # @classproperty # def details_template(cls) -> Template: @@ -2410,8 +2473,7 @@ class EquipmentRole(BaseClass): pass return cls.execute_query(query=query, limit=limit) - def get_processes(self, proceduretype: str | ProcedureType | None, - kittype: str | KitType | None = None) -> Generator[Process, None, None]: + def get_processes(self, proceduretype: str | ProcedureType | None) -> Generator[Process, None, None]: """ Get process used by this EquipmentRole @@ -2424,13 +2486,13 @@ class EquipmentRole(BaseClass): """ if isinstance(proceduretype, str): proceduretype = SubmissionType.query(name=proceduretype) - if isinstance(kittype, str): - kittype = KitType.query(name=kittype) + # if isinstance(kittype, str): + # kittype = KitType.query(name=kittype) for process in self.process: if proceduretype and proceduretype not in process.proceduretype: continue - if kittype and kittype not in process.kittype: - continue + # if kittype and kittype not in process.kittype: + # continue yield process.name def to_omni(self, expand: bool = False) -> "OmniEquipmentRole": @@ -2452,21 +2514,31 @@ class EquipmentRole(BaseClass): output = super().details_dict(**kwargs) # Note con output['equipment'] = [item.details_dict() for item in output['equipment']] - output['equipment_json'] = [] + # output['equipment_json'] = [] equip = [] for eq in output['equipment']: dicto = dict(name=eq['name'], asset_number=eq['asset_number']) - dicto['processes'] = [dict(name=process.name, tiprole=process.tiprole) for process in eq['process'] if proceduretype in process.proceduretype] - for process in dicto['processes']: + + dicto['process'] = [ + {'name': version.name, 'tiprole': process.tiprole} + for process in eq['process'] + if proceduretype in process.proceduretype + for version in process.processversion + ] + for process in dicto['process']: try: - process['tips'] = flatten_list([[tips.name for tips in tr.tips]for tr in process['tiprole']]) + process['tips'] = flatten_list([[tips.name for tips in tr.tips] for tr in process['tiprole']]) except KeyError: logger.debug(f"process: {pformat(process)}") raise KeyError() del process['tiprole'] equip.append(dicto) - output['equipment_json'].append(dict(name=self.name, equipment=equip)) - output['process'] = [item.details_dict() for item in output['process']] + output['equipment'] = equip + # output['equipment_json'].append(dict(name=self.name, equipment=equip)) + # output['process'] = [item.details_dict() for item in output['process']] + output['process'] = [version.details_dict() for version in + flatten_list([process.processversion for process in self.process])] + logger.debug(f"\n\nProcess: {pformat(output['process'])}") try: output['tips'] = [item.details_dict() for item in output['tips']] except KeyError: @@ -2483,8 +2555,8 @@ class ProcedureEquipmentAssociation(BaseClass): equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure equipmentrole = Column(String(64), primary_key=True) #: name of the reagentrole the equipment fills - process_id = Column(INTEGER, ForeignKey("_process.id", ondelete="SET NULL", - name="SEA_Process_id")) #: Foreign key of process id + processversion_id = Column(INTEGER, ForeignKey("_processversion.id", ondelete="SET NULL", + name="SEA_Process_id")) #: Foreign key of process id start_time = Column(TIMESTAMP) #: start time of equipment use end_time = Column(TIMESTAMP) #: end time of equipment use comments = Column(String(1024)) #: comments about equipment @@ -2495,11 +2567,11 @@ class ProcedureEquipmentAssociation(BaseClass): equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete="SET NULL", - name="SEA_Process_id")) + name="SEA_Process_id")) def __repr__(self) -> str: try: - return f"" + return f"" except AttributeError: return "" @@ -2529,7 +2601,7 @@ class ProcedureEquipmentAssociation(BaseClass): @property def process(self): - return Process.query(id=self.process_id) + return ProcessVersion.query(id=self.processversion_id) @property def tips(self): @@ -2614,8 +2686,8 @@ class ProcedureTypeEquipmentRoleAssociation(BaseClass): equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), primary_key=True) #: id of associated procedure - kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), - primary_key=True) + # kittype_id = Column(INTEGER, ForeignKey("_kittype.id"), + # primary_key=True) uses = Column(JSON) #: locations of equipment on the procedure 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? @@ -2626,7 +2698,7 @@ class ProcedureTypeEquipmentRoleAssociation(BaseClass): equipmentrole = relationship(EquipmentRole, back_populates="equipmentroleproceduretypeassociation") #: associated equipment - kittype = relationship(KitType, back_populates="proceduretypeequipmentroleassociation") + # kittype = relationship(KitType, back_populates="proceduretypeequipmentroleassociation") @validates('static') def validate_static(self, key, value): @@ -2667,13 +2739,14 @@ class Process(BaseClass): secondary=equipment_process) #: relation to Equipment equipmentrole = relationship("EquipmentRole", back_populates='process', secondary=equipmentrole_process) #: relation to EquipmentRoles - procedure = relationship("ProcedureEquipmentAssociation", - backref='process') #: relation to RunEquipmentAssociation - kittype = relationship("KitType", back_populates='process', - secondary=kittype_process) #: relation to KitType + + # kittype = relationship("KitType", back_populates='process', + # secondary=kittype_process) #: relation to KitType tiprole = relationship("TipRole", back_populates='process', secondary=process_tiprole) #: relation to KitType + processversion = relationship("ProcessVersion", back_populates="process") + def set_attribute(self, key, value): match key: case "name": @@ -2703,7 +2776,7 @@ class Process(BaseClass): name: str | None = None, id: int | None = None, proceduretype: str | ProcedureType | None = None, - kittype: str | KitType | None = None, + # kittype: str | KitType | None = None, equipmentrole: str | EquipmentRole | None = None, limit: int = 0, **kwargs) -> Process | List[Process]: @@ -2727,14 +2800,7 @@ class Process(BaseClass): query = query.filter(cls.proceduretype.contains(proceduretype)) case _: pass - match kittype: - case str(): - kittype = KitType.query(name=kittype) - query = query.filter(cls.kittype.contains(kittype)) - case KitType(): - query = query.filter(cls.kittype.contains(kittype)) - case _: - pass + match equipmentrole: case str(): equipmentrole = EquipmentRole.query(name=equipmentrole) @@ -2779,24 +2845,9 @@ class Process(BaseClass): tiprole=tiprole ) - def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict: - """ - dictionary containing values necessary for gui - - Args: - full_data (bool, optional): Whether to include procedure in data for details. Defaults to False. - - Returns: - dict: representation of the equipment's attributes - """ - output = dict( - name=self.name, - ) - if full_data: - subs = [dict(plate=sub.run.rsl_plate_number, equipment=sub.equipment.name, - submitted_date=sub.run.clientsubmission.submitted_date) for sub in self.procedure] - output['procedure'] = sorted(subs, key=itemgetter("submitted_date"), reverse=True) - output['excluded'] = ['missing', 'procedure', 'excluded', 'editable'] + def details_dict(self, **kwargs): + output = super().details_dict(**kwargs) + output['processversion'] = [item.details_dict() for item in self.processversion] return output def to_pydantic(self): @@ -2812,25 +2863,27 @@ class Process(BaseClass): return PydProcess(**output) - # @classproperty - # def details_template(cls) -> Template: - # """ - # Get the details jinja template for the correct class - # - # Args: - # base_dict (dict): incoming dictionary of Submission fields - # - # Returns: - # Tuple(dict, Template): (Updated dictionary, Template to be rendered) - # """ - # env = jinja_template_loading() - # temp_name = f"{cls.__name__.lower()}_details.html" - # try: - # template = env.get_template(temp_name) - # except TemplateNotFound as e: - # logger.error(f"Couldn't find template {e}") - # template = env.get_template("process_details.html") - # return template +class ProcessVersion(BaseClass): + id = Column(INTEGER, primary_key=True) #: Process id, primary key + version = Column(FLOAT(2), default=1.00) + date_verified = Column(TIMESTAMP) + project = Column(String(128)) + process = relationship("Process", back_populates="processversion") + process_id = Column(INTEGER, ForeignKey("_process.id", ondelete="SET NULL", + name="fk_version_process_id")) + procedure = relationship("ProcedureEquipmentAssociation", + backref='process') #: relation to RunEquipmentAssociation + + @property + def name(self) -> str: + return f"{self.process.name}-v{str(self.version)}" + + def details_dict(self, **kwargs): + output = super().details_dict(**kwargs) + output['name'] = self.name + if not output['project']: + output['project'] = "" + return output class TipRole(BaseClass): @@ -2849,7 +2902,9 @@ class TipRole(BaseClass): cascade="all, delete-orphan" ) #: associated procedure - proceduretype = association_proxy("tiproleproceduretypeassociation", "proceduretype") + proceduretype = association_proxy("tiproleproceduretypeassociation", "proceduretype", + creator=lambda proceduretype: ProcedureTypeTipRoleAssociation( + proceduretype=proceduretype)) # @classmethod # def query_or_create(cls, **kwargs) -> Tuple[TipRole, bool]: @@ -2906,8 +2961,10 @@ class Tips(BaseClass, LogMixin): secondary=tiprole_tips) #: joined parent reagent type tiprole_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 + manufacturer = Column(String(64)) + capacity = Column(INTEGER) + ref = Column(String(64)) #: tip reference number + # lot = Column(String(64)) #: lot number of tips equipment = relationship("Equipment", back_populates="tips", secondary=equipment_tips) #: associated procedure tipsprocedureassociation = relationship( @@ -2918,6 +2975,14 @@ class Tips(BaseClass, LogMixin): procedure = association_proxy("tipsprocedureassociation", 'procedure') + @property + def size(self) -> str: + return f"{self.capacity}ul" + + @property + def name (self) -> str: + return f"{self.manufacturer}-{self.size}-{self.ref}" + # @classmethod # def query_or_create(cls, **kwargs) -> Tuple[Tips, bool]: # new = False @@ -3129,7 +3194,7 @@ class Results(BaseClass): return None @property - def image(self) -> bytes|None: + def image(self) -> bytes | None: dir = self.__directory_path__.joinpath("submission_imgs.zip") try: assert dir.exists() @@ -3144,9 +3209,8 @@ class Results(BaseClass): def image(self, value): self._img = value - def to_pydantic(self, pyd_model_name:str|None=None, **kwargs): + def to_pydantic(self, pyd_model_name: str | None = None, **kwargs): output = super().to_pydantic(pyd_model_name=pyd_model_name, **kwargs) if self.sample_id: output.sample_id = self.sample_id return output - diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 8c1b705..bb14676 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -20,9 +20,7 @@ from pandas import DataFrame from sqlalchemy.ext.hybrid import hybrid_property from frontend.widgets.functions import select_save_file -from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin, Procedure, \ - kittype_procedure - +from . import Base, BaseClass, Reagent, SubmissionType, ClientLab, Contact, LogMixin, Procedure from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table, Sequence from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm.attributes import flag_modified @@ -42,7 +40,7 @@ from jinja2 import Template from PIL import Image if TYPE_CHECKING: - from backend.db.models.kits import ProcedureType, Procedure + from backend.db.models.procedures import ProcedureType, Procedure logger = logging.getLogger(f"submissions.{__name__}") @@ -871,8 +869,8 @@ class Run(BaseClass, LogMixin): value (_type_): value of attribute """ match key: - case "kittype": - field_value = KitType.query(name=value) + # case "kittype": + # field_value = KitType.query(name=value) case "clientlab": field_value = ClientLab.query(name=value) case "contact": @@ -1241,25 +1239,12 @@ class Run(BaseClass, LogMixin): def add_procedure(self, obj, proceduretype_name: str): from frontend.widgets.procedure_creation import ProcedureCreation - procedure_type = next( + procedure_type: ProcedureType = next( (proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name)) logger.debug(f"Got ProcedureType: {procedure_type}") dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self)) if dlg.exec(): sql, _ = dlg.return_sql(new=True) - # logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}") - # previous = [proc for proc in self.procedure if proc.proceduretype == procedure_type] - # repeats = len([proc for proc in previous if proc.repeat]) - # if sql.repeat: - # repeats += 1 - # if repeats > 0: - # suffix = f"-{str(len(previous))}R{repeats}" - # else: - # suffix = f"-{str(len(previous)+1)}" - # sql.name = f"{sql.repeat}{suffix}" - # else: - # suffix = f"-{str(len(previous)+1)}" - # sql.name = f"{self.name}-{proceduretype_name}{suffix}" sql.save() obj.set_data() diff --git a/src/submissions/backend/excel/__init__.py b/src/submissions/backend/excel/__init__.py index 5df75cf..4df4d82 100644 --- a/src/submissions/backend/excel/__init__.py +++ b/src/submissions/backend/excel/__init__.py @@ -2,7 +2,7 @@ Contains pandas and openpyxl convenience functions for interacting with excel workbooks ''' -from .parser import * -from backend.excel.parsers.clientsubmission_parser import * -from .reports import * -from .writer import * +# from .parser import * +from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser +# from .reports import * +# from .writer import * diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py deleted file mode 100644 index 2562801..0000000 --- a/src/submissions/backend/excel/parser.py +++ /dev/null @@ -1,698 +0,0 @@ -""" -contains clientsubmissionparser objects for pulling values from client generated procedure sheets. -""" -import logging -from copy import copy -from getpass import getuser -from pprint import pformat -from typing import List -from openpyxl import load_workbook, Workbook -from pathlib import Path -from backend.db.models import * -from backend.validators import PydRun, RSLNamer -from collections import OrderedDict -from tools import check_not_nan, is_missing, check_key_or_attr - -logger = logging.getLogger(f"submissions.{__name__}") - - -class SheetParser(object): - """ - object to pull and contain data from excel file - """ - - def __init__(self, filepath: Path | None = None): - """ - Args: - filepath (Path | None, optional): file path to excel sheet. Defaults to None. - """ - logger.info(f"\n\nParsing {filepath.__str__()}\n\n") - match filepath: - case Path(): - self.filepath = filepath - case str(): - self.filepath = Path(filepath) - case _: - logger.error(f"No filepath given.") - raise ValueError("No filepath given.") - try: - self.xl = load_workbook(filepath, data_only=True) - except ValueError as e: - logger.error(f"Incorrect value: {e}") - raise FileNotFoundError(f"Couldn't parse file {self.filepath}") - self.sub = OrderedDict() - # NOTE: make decision about type of sample we have - self.sub['proceduretype'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath), - missing=True) - self.submission_type = SubmissionType.query(name=self.sub['proceduretype']) - self.sub_object = Run.find_polymorphic_subclass(polymorphic_identity=self.submission_type) - # NOTE: grab the info map from the procedure type in database - self.parse_info() - self.import_kit_validation_check() - self.parse_reagents() - self.parse_samples() - self.parse_equipment() - self.parse_tips() - - def parse_info(self): - """ - Pulls basic information from the excel sheet - """ - 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 procedure types we have to check for the type in the excel sheet and rerun accordingly - try: - check = parser.parsed_info['proceduretype']['value'] not in [None, "None", "", " "] - except KeyError as e: - logger.error(f"Couldn't check procedure type due to KeyError: {e}") - return - logger.info( - f"Checking for updated procedure type: {self.submission_type.name} against new: {parser.parsed_info['proceduretype']['value']}") - if self.submission_type.name != parser.parsed_info['proceduretype']['value']: - if check: - # NOTE: If initial procedure type doesn't match parsed procedure type, defer to parsed procedure type. - self.submission_type = SubmissionType.query(name=parser.parsed_info['proceduretype']['value']) - logger.info(f"Updated self.proceduretype to {self.submission_type}. Rerunning parse.") - self.parse_info() - else: - self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath) - self.parse_info() - for k, v in parser.parsed_info.items(): - self.sub.__setitem__(k, v) - - def parse_reagents(self, extraction_kit: str | None = None): - """ - Calls reagent clientsubmissionparser class to pull info from the excel sheet - - Args: - extraction_kit (str | None, optional): Relevant extraction kittype for reagent map. Defaults to None. - """ - if extraction_kit is None: - extraction_kit = self.sub['kittype'] - parser = ReagentParser(xl=self.xl, submission_type=self.submission_type, - extraction_kit=extraction_kit) - self.sub['reagents'] = parser.parsed_reagents - - def parse_samples(self): - """ - Calls sample clientsubmissionparser to pull info from the excel sheet - """ - parser = SampleParser(xl=self.xl, submission_type=self.submission_type) - self.sub['sample'] = parser.parsed_samples - - def parse_equipment(self): - """ - Calls equipment clientsubmissionparser to pull info from the excel sheet - """ - parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type) - self.sub['equipment'] = parser.parsed_equipment - - def parse_tips(self): - """ - Calls tips clientsubmissionparser to pull info from the excel sheet - """ - parser = TipParser(xl=self.xl, submission_type=self.submission_type) - self.sub['tips'] = parser.parsed_tips - - def import_kit_validation_check(self): - """ - Enforce that the clientsubmissionparser has an extraction kittype - """ - if 'kittype' not in self.sub.keys() or not check_not_nan(self.sub['kittype']['value']): - from frontend.widgets.pop_ups import ObjectSelector - dlg = ObjectSelector(title="Kit Needed", message="At minimum a kittype is needed. Please select one.", - obj_type=KitType) - if dlg.exec(): - self.sub['kittype'] = dict(value=dlg.parse_form(), missing=True) - else: - raise ValueError("Extraction kittype needed.") - else: - if isinstance(self.sub['kittype'], str): - self.sub['kittype'] = dict(value=self.sub['kittype'], missing=True) - - def to_pydantic(self) -> PydRun: - """ - Generates a pydantic model of scraped data for validation - - Returns: - PydSubmission: output pydantic model - """ - return PydRun(filepath=self.filepath, run_custom=True, **self.sub) - - -class InfoParser(object): - """ - Object to parse generic info from excel sheet. - """ - - def __init__(self, xl: Workbook, submission_type: str | SubmissionType, sub_object: Run | None = None): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (str | SubmissionType): Type of procedure 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 = Run.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 - self.xl = xl - - @property - def info_map(self) -> dict: - """ - Gets location of basic info from the proceduretype object in the database. - - Returns: - dict: Location map of all info for this procedure type - """ - # NOTE: Get the parse_info method from the procedure type specified - return self.sub_object.construct_info_map(submission_type=self.submission_type_obj, mode="read") - - @property - def parsed_info(self) -> dict: - """ - Pulls basic info from the excel sheet. - - Returns: - dict: key:value of basic info - """ - dicto = {} - # NOTE: This loop parses generic info - for sheet in self.xl.sheetnames: - ws = self.xl[sheet] - relevant = [] - for k, v in self.info_map.items(): - # NOTE: If the value is hardcoded put it in the dictionary directly. Ex. Artic kittype - if k == "custom": - continue - if isinstance(v, str): - dicto[k] = dict(value=v, missing=False) - continue - for location in v: - try: - check = location['sheet'] == sheet - except TypeError: - logger.warning(f"Location is likely a string, skipping") - dicto[k] = dict(value=location, missing=False) - check = False - if check: - new = location - new['name'] = k - relevant.append(new) - # NOTE: make sure relevant is not an empty list. - if not relevant: - continue - for item in relevant: - # NOTE: Get cell contents at this location - value = ws.cell(row=item['row'], column=item['column']).value - match item['name']: - case "proceduretype": - value, missing = is_missing(value) - value = value.title() - case "submitted_date": - value, missing = is_missing(value) - # NOTE: is field a JSON? Includes: Extraction info, PCR info, comment, custom - case thing if thing in self.sub_object.jsons: - value, missing = is_missing(value) - if missing: continue - value = dict(name=f"Parser_{sheet}", text=value, time=datetime.now()) - try: - dicto[item['name']]['value'] += value - continue - except KeyError: - logger.error(f"New value for {item['name']}") - case _: - value, missing = is_missing(value) - if item['name'] not in dicto.keys(): - try: - dicto[item['name']] = dict(value=value, missing=missing) - except (KeyError, IndexError): - continue - # NOTE: Return after running the clientsubmissionparser components held in procedure object. - return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl, custom_fields=self.info_map['custom']) - - -class ReagentParser(object): - """ - Object to pull reagents from excel sheet. - """ - - def __init__(self, xl: Workbook, submission_type: str | SubmissionType, extraction_kit: str, - run_object: Run | None = None): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (str|SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - extraction_kit (str): Extraction kittype used. - 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 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) - self.xl = xl - - @property - def kit_map(self) -> dict: - """ - Gets location of kittype reagents from database - - Args: - proceduretype (str): Name of procedure type. - - Returns: - dict: locations of reagent info for the kittype. - """ - associations, self.kit_object = self.kit_object.construct_xl_map_for_use( - proceduretype=self.submission_type_obj) - reagent_map = {k: v for k, v in associations.items() if k != 'info'} - try: - del reagent_map['info'] - except KeyError: - pass - return reagent_map - - @property - def parsed_reagents(self) -> Generator[dict, None, None]: - """ - Extracts reagent information from the Excel form. - - Returns: - List[PydReagent]: List of parsed reagents. - """ - for sheet in self.xl.sheetnames: - ws = self.xl[sheet] - relevant = {k.strip(): v for k, v in self.kit_map.items() if sheet in self.kit_map[k]['sheet']} - if not relevant: - continue - for item in relevant: - try: - reagent = relevant[item] - name = ws.cell(row=reagent['name']['row'], column=reagent['name']['column']).value - lot = ws.cell(row=reagent['lot']['row'], column=reagent['lot']['column']).value - expiry = ws.cell(row=reagent['expiry']['row'], column=reagent['expiry']['column']).value - if 'comment' in relevant[item].keys(): - comment = ws.cell(row=reagent['comment']['row'], column=reagent['comment']['column']).value - else: - comment = "" - except (KeyError, IndexError): - yield dict(role=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True) - # NOTE: If the cell is blank tell the PydReagent - if check_not_nan(lot): - missing = False - else: - missing = True - lot = str(lot) - try: - check = name.lower() != "not applicable" - except AttributeError: - logger.warning(f"name is not a string.") - check = True - if check: - yield dict(role=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, - missing=missing) - - -class SampleParser(object): - """ - Object to pull data for sample in excel sheet and construct individual sample objects - """ - - def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, - sub_object: Run | None = None) -> None: - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - sample_map (dict | None, optional): Locations in database where sample are found. Defaults to None. - sub_object (BasicRun | None, optional): Submission object holding methods. Defaults to None. - """ - self.samples = [] - self.xl = xl - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type.name - self.submission_type_obj = submission_type - if sub_object is None: - logger.warning( - f"Sample clientsubmissionparser attempting to fetch procedure class with polymorphic identity: {self.submission_type}") - sub_object = Run.find_polymorphic_subclass(polymorphic_identity=self.submission_type) - self.sub_object = sub_object - self.sample_type = self.sub_object.get_default_info("sampletype", submission_type=submission_type) - self.samp_object = Sample.find_polymorphic_subclass(polymorphic_identity=self.sample_type) - - @property - def sample_map(self) -> dict: - """ - Gets info locations in excel book for procedure type. - - Args: - proceduretype (str): procedure type - - Returns: - dict: Info locations. - """ - - return self.sub_object.construct_sample_map(submission_type=self.submission_type_obj) - - @property - def plate_map_samples(self) -> List[dict]: - """ - Parse sample location/name from plate map - - Returns: - List[dict]: List of sample ids and locations. - """ - invalids = [0, "0", "EMPTY"] - smap = self.sample_map['plate_map'] - ws = self.xl[smap['sheet']] - plate_map_samples = [] - for ii, row in enumerate(range(smap['start_row'], smap['end_row'] + 1), start=1): - for jj, column in enumerate(range(smap['start_column'], smap['end_column'] + 1), start=1): - id = str(ws.cell(row=row, column=column).value) - if check_not_nan(id): - if id not in invalids: - sample_dict = dict(id=id, row=ii, column=jj) - sample_dict['sampletype'] = self.sample_type - plate_map_samples.append(sample_dict) - else: - pass - else: - pass - return plate_map_samples - - @property - def lookup_samples(self) -> List[dict]: - """ - Parse misc info from lookup table. - - Returns: - List[dict]: List of basic sample info. - """ - - lmap = self.sample_map['lookup_table'] - ws = self.xl[lmap['sheet']] - lookup_samples = [] - for ii, row in enumerate(range(lmap['start_row'], lmap['end_row'] + 1), start=1): - row_dict = {k: ws.cell(row=row, column=v).value for k, v in lmap['sample_columns'].items()} - try: - row_dict[lmap['merge_on_id']] = str(row_dict[lmap['merge_on_id']]) - except KeyError: - pass - row_dict['sampletype'] = self.sample_type - row_dict['submission_rank'] = ii - try: - check = check_not_nan(row_dict[lmap['merge_on_id']]) - except KeyError: - check = False - if check: - lookup_samples.append(self.samp_object.parse_sample(row_dict)) - return lookup_samples - - @property - def parsed_samples(self) -> Generator[dict, None, None]: - """ - Merges sample info from lookup table and plate map. - - Returns: - List[dict]: Reconciled sample - """ - if not self.plate_map_samples or not self.lookup_samples: - logger.warning(f"No separate sample") - samples = self.lookup_samples or self.plate_map_samples - for new in samples: - if not check_key_or_attr(key='sample_id', interest=new, check_none=True): - new['sample_id'] = new['id'] - new = self.sub_object.parse_samples(new) - try: - del new['id'] - except KeyError: - pass - yield new - else: - merge_on_id = self.sample_map['lookup_table']['merge_on_id'] - logger.info(f"Merging sample info using {merge_on_id}") - plate_map_samples = sorted(copy(self.plate_map_samples), key=itemgetter('id')) - lookup_samples = sorted(copy(self.lookup_samples), key=itemgetter(merge_on_id)) - for ii, psample in enumerate(plate_map_samples): - # NOTE: See if we can do this the easy way and just use the same list index. - try: - check = psample['id'] == lookup_samples[ii][merge_on_id] - except (KeyError, IndexError): - check = False - if check: - new = lookup_samples[ii] | psample - lookup_samples[ii] = {} - else: - logger.warning(f"Match for {psample['id']} not direct, running search.") - searchables = [(jj, sample) for jj, sample in enumerate(lookup_samples) - if merge_on_id in sample.keys()] - jj, new = next(((jj, lsample | psample) for jj, lsample in searchables - if lsample[merge_on_id] == psample['id']), (-1, psample)) - if jj >= 0: - lookup_samples[jj] = {} - if not check_key_or_attr(key='sample_id', interest=new, check_none=True): - new['sample_id'] = psample['id'] - new = self.sub_object.parse_samples(new) - try: - del new['id'] - except KeyError: - pass - yield new - - -class EquipmentParser(object): - """ - Object to pull data for equipment in excel sheet - """ - - def __init__(self, xl: Workbook, submission_type: str | SubmissionType) -> None: - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (str | SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - """ - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type - self.xl = xl - - @property - def equipment_map(self) -> dict: - """ - Gets the map of equipment locations in the procedure type's spreadsheet - - Returns: - List[dict]: List of locations - """ - return {k: v for k, v in self.submission_type.construct_field_map("equipment")} - - def get_asset_number(self, input: str) -> str: - """ - Pulls asset number from string. - - Args: - input (str): String to be scraped - - Returns: - str: asset number - """ - regex = Equipment.manufacturer_regex - try: - return regex.search(input).group().strip("-") - except AttributeError as e: - logger.error(f"Error getting asset number for {input}: {e}") - return input - - @property - def parsed_equipment(self) -> Generator[dict, None, None]: - """ - Scrapes equipment from xl sheet - - Returns: - List[dict]: list of equipment - """ - for sheet in self.xl.sheetnames: - ws = self.xl[sheet] - try: - relevant = {k: v for k, v in self.equipment_map.items() if v['sheet'] == sheet} - except (TypeError, KeyError) as e: - logger.error(f"Error creating relevant equipment list: {e}") - continue - previous_asset = "" - for k, v in relevant.items(): - asset = ws.cell(v['name']['row'], v['name']['column']).value - if not check_not_nan(asset): - asset = previous_asset - else: - previous_asset = asset - asset = self.get_asset_number(input=asset) - eq = Equipment.query(asset_number=asset) - if eq is None: - eq = Equipment.query(name=asset) - process = ws.cell(row=v['process']['row'], column=v['process']['column']).value - try: - yield dict(name=eq.name, processes=[process], role=k, asset_number=eq.asset_number, - nickname=eq.nickname) - except AttributeError: - logger.error(f"Unable to add {eq} to list.") - continue - - -class TipParser(object): - """ - Object to pull data for tips in excel sheet - """ - - def __init__(self, xl: Workbook, submission_type: str | SubmissionType) -> None: - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (str | SubmissionType): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - """ - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type - self.xl = xl - - @property - def tip_map(self) -> dict: - """ - Gets the map of equipment locations in the procedure type's spreadsheet - - Returns: - List[dict]: List of locations - """ - return {k: v for k, v in self.submission_type.construct_field_map("tip")} - - @property - def parsed_tips(self) -> Generator[dict, None, None]: - """ - Scrapes equipment from xl sheet - - Returns: - List[dict]: list of equipment - """ - for sheet in self.xl.sheetnames: - ws = self.xl[sheet] - try: - relevant = {k: v for k, v in self.tip_map.items() if v['sheet'] == sheet} - except (TypeError, KeyError) as e: - logger.error(f"Error creating relevant equipment list: {e}") - continue - previous_asset = "" - for k, v in relevant.items(): - asset = ws.cell(v['name']['row'], v['name']['column']).value - if "lot" in v.keys(): - lot = ws.cell(v['lot']['row'], v['lot']['column']).value - else: - lot = None - if not check_not_nan(asset): - asset = previous_asset - else: - previous_asset = asset - eq = Tips.query(lot=lot, name=asset, limit=1) - try: - yield dict(name=eq.name, role=k, lot=lot) - except AttributeError: - logger.error(f"Unable to add {eq} to PydTips list.") - - -class PCRParser(object): - """Object to pull data from Design and Analysis PCR export file.""" - - def __init__(self, filepath: Path | None = None, submission: Run | None = None) -> None: - """ - Args: - filepath (Path | None, optional): file to parse. Defaults to None. - submission (BasicRun | None, optional): Submission parsed data to be added to. - """ - if filepath is None: - logger.error('No filepath given.') - self.xl = None - else: - try: - self.xl = load_workbook(filepath) - except ValueError as e: - logger.error(f'Incorrect value: {e}') - self.xl = None - 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 = Wastewater - rsl_plate_number = None - else: - self.submission_obj = submission - rsl_plate_number = self.submission_obj.rsl_plate_number - self.samples = self.submission_obj.parse_pcr(xl=self.xl, rsl_plate_number=rsl_plate_number) - self.controls = self.submission_obj.parse_pcr_controls(xl=self.xl, rsl_plate_number=rsl_plate_number) - - @property - def pcr_info(self) -> dict: - """ - Parse general info rows for all types of PCR results - """ - info_map = self.submission_obj.get_submission_type().sample_map['pcr_general_info'] - sheet = self.xl[info_map['sheet']] - iter_rows = sheet.iter_rows(min_row=info_map['start_row'], max_row=info_map['end_row']) - pcr = {} - for row in iter_rows: - try: - key = row[0].value.lower().replace(' ', '_') - except AttributeError as e: - logger.error(f"No key: {row[0].value} due to {e}") - continue - value = row[1].value or "" - pcr[key] = value - pcr['imported_by'] = getuser() - return pcr - - -class ConcentrationParser(object): - - def __init__(self, filepath: Path | None = None, run: Run | None = None) -> None: - if filepath is None: - logger.error('No filepath given.') - self.xl = None - else: - try: - self.xl = load_workbook(filepath) - except ValueError as e: - logger.error(f'Incorrect value: {e}') - self.xl = None - except PermissionError: - logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.") - return None - if run is None: - self.submission_obj = Run() - rsl_plate_number = None - else: - self.submission_obj = run - rsl_plate_number = self.submission_obj.rsl_plate_number - self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_number=rsl_plate_number) - -# NOTE: Generified parsers below - -class InfoParserV2(object): - """ - Object for retrieving submitter info from sample list sheet - """ - - default_range = dict( - start_row=2, - end_row=18, - start_column=7, - end_column=8, - sheet="Sample List" - ) - diff --git a/src/submissions/backend/excel/parsers/clientsubmission_parser.py b/src/submissions/backend/excel/parsers/clientsubmission_parser.py index c3a4e95..10cd7c9 100644 --- a/src/submissions/backend/excel/parsers/clientsubmission_parser.py +++ b/src/submissions/backend/excel/parsers/clientsubmission_parser.py @@ -77,8 +77,10 @@ class SubmissionTyperMixin(object): SubmissionType: The determined submissiontype """ from backend.db.models import SubmissionType - parser = ClientSubmissionInfoParser(filepath=filepath, submissiontype=SubmissionType.query(name="Test")) + parser = ClientSubmissionInfoParser(filepath=filepath, submissiontype=SubmissionType.query(name="Default")) sub_type = next((value for k, value in parser.parsed_info.items() if k == "submissiontype" or k == "submission_type"), None) + if isinstance(sub_type, dict): + sub_type = sub_type['value'] sub_type = SubmissionType.query(name=sub_type.title()) if isinstance(sub_type, list): return diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py deleted file mode 100644 index 15d214d..0000000 --- a/src/submissions/backend/excel/writer.py +++ /dev/null @@ -1,500 +0,0 @@ -""" -contains writer objects for pushing values to procedure sheet templates. -""" -import logging -from copy import copy -from datetime import datetime -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, Run -from backend.validators.pydant import PydRun -from io import BytesIO -from collections import OrderedDict - -logger = logging.getLogger(f"submissions.{__name__}") - - -class SheetWriter(object): - """ - object to manage data placement into excel file - """ - - def __init__(self, submission: PydRun): - """ - Args: - submission (PydSubmission): Object containing procedure information. - """ - self.sub = OrderedDict(submission.improved_dict()) - # NOTE: Set values from pydantic object. - for k, v in self.sub.items(): - match k: - case 'filepath': - self.__setattr__(k, v) - case 'proceduretype': - self.sub[k] = v['value'] - self.submission_type = SubmissionType.query(name=v['value']) - self.run_object = Run.find_polymorphic_subclass( - polymorphic_identity=self.submission_type) - case _: - if isinstance(v, dict): - self.sub[k] = v['value'] - else: - self.sub[k] = v - template = self.submission_type.template_file - if not template: - logger.error(f"No template file found, falling back to Bacterial Culture") - template = SubmissionType.basic_template - workbook = load_workbook(BytesIO(template)) - self.xl = workbook - self.write_info() - self.write_reagents() - self.write_samples() - self.write_equipment() - self.write_tips() - - def write_info(self): - """ - Calls info writer - """ - disallowed = ['filepath', 'reagents', 'sample', 'equipment', 'control'] - info_dict = {k: v for k, v in self.sub.items() if k not in disallowed} - writer = InfoWriter(xl=self.xl, submission_type=self.submission_type, info_dict=info_dict) - self.xl = writer.write_info() - - def write_reagents(self): - """ - Calls reagent writer - """ - reagent_list = self.sub['reagents'] - writer = ReagentWriter(xl=self.xl, submission_type=self.submission_type, - extraction_kit=self.sub['kittype'], reagent_list=reagent_list) - self.xl = writer.write_reagents() - - def write_samples(self): - """ - Calls sample writer - """ - sample_list = self.sub['sample'] - writer = SampleWriter(xl=self.xl, submission_type=self.submission_type, sample_list=sample_list) - self.xl = writer.write_samples() - - def write_equipment(self): - """ - Calls equipment writer - """ - equipment_list = self.sub['equipment'] - writer = EquipmentWriter(xl=self.xl, submission_type=self.submission_type, equipment_list=equipment_list) - self.xl = writer.write_equipment() - - def write_tips(self): - """ - Calls tip writer - """ - tips_list = self.sub['tips'] - writer = TipWriter(xl=self.xl, submission_type=self.submission_type, tips_list=tips_list) - self.xl = writer.write_tips() - - -class InfoWriter(object): - """ - object to write general procedure info into excel file - """ - - def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, - sub_object: Run | None = None): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - info_dict (dict): Dictionary of information to write. - 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 = Run.find_polymorphic_subclass(polymorphic_identity=submission_type.name) - self.submission_type = submission_type - self.sub_object = sub_object - self.xl = xl - self.info_map = submission_type.construct_info_map(mode='write') - self.info = self.reconcile_map(info_dict, self.info_map) - - def reconcile_map(self, info_dict: dict, info_map: dict) -> Generator[(Tuple[str, dict]), None, None]: - """ - Merge info with its locations - - Args: - info_dict (dict): dictionary of info items - info_map (dict): dictionary of info locations - - Returns: - dict: merged dictionary - """ - for k, v in info_dict.items(): - if v is None: - continue - if k == "custom": - continue - dicto = {} - try: - dicto['locations'] = info_map[k] - except KeyError: - pass - dicto['value'] = v - if len(dicto) > 0: - yield k, dicto - - def write_info(self) -> Workbook: - """ - Performs write operations - - Returns: - Workbook: workbook with info written. - """ - final_info = {} - for k, v in self.info: - match k: - case "custom": - continue - case "comment": - # NOTE: merge all comments to fit in single cell. - if isinstance(v['value'], list): - json_join = [item['text'] for item in v['value'] if 'text' in item.keys()] - v['value'] = "\n".join(json_join) - case thing if thing in self.sub_object.timestamps: - v['value'] = v['value'].date() - case _: - pass - final_info[k] = v - try: - locations = v['locations'] - except KeyError: - logger.error(f"No locations for {k}, skipping") - continue - for loc in locations: - sheet = self.xl[loc['sheet']] - try: - # logger.debug(f"Writing {v['value']} to row {loc['row']} and column {loc['column']}") - sheet.cell(row=loc['row'], column=loc['column'], value=v['value']) - except AttributeError as e: - logger.error(f"Can't write {k} to that cell due to AttributeError: {e}") - except ValueError as e: - logger.error(f"Can't write {v} to that cell due to ValueError: {e}") - sheet.cell(row=loc['row'], column=loc['column'], value=v['value'].name) - return self.sub_object.custom_info_writer(self.xl, info=final_info, custom_fields=self.info_map['custom']) - - -class ReagentWriter(object): - """ - object to write reagent data into excel file - """ - - def __init__(self, xl: Workbook, submission_type: SubmissionType | str, extraction_kit: KitType | str, - reagent_list: list): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - extraction_kit (KitType | str): Extraction kittype used. - reagent_list (list): List of reagent dicts to be written to excel. - """ - self.xl = xl - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type_obj = submission_type - if isinstance(extraction_kit, str): - extraction_kit = KitType.query(name=extraction_kit) - self.kit_object = extraction_kit - associations, self.kit_object = self.kit_object.construct_xl_map_for_use( - proceduretype=self.submission_type_obj) - reagent_map = {k: v for k, v in associations.items()} - self.reagents = self.reconcile_map(reagent_list=reagent_list, reagent_map=reagent_map) - - def reconcile_map(self, reagent_list: List[dict], reagent_map: dict) -> Generator[dict, None, None]: - """ - Merge reagents with their locations - - Args: - reagent_list (List[dict]): List of reagent dictionaries - reagent_map (dict): Reagent locations - - Returns: - List[dict]: merged dictionary - """ - filled_roles = [item['reagentrole'] for item in reagent_list] - for map_obj in reagent_map.keys(): - if map_obj not in filled_roles: - reagent_list.append(dict(name="Not Applicable", role=map_obj, lot="Not Applicable", expiry="Not Applicable")) - for reagent in reagent_list: - try: - mp_info = reagent_map[reagent['reagentrole']] - except KeyError: - continue - placeholder = copy(reagent) - for k, v in reagent.items(): - try: - dicto = dict(value=v, row=mp_info[k]['row'], column=mp_info[k]['column']) - except KeyError as e: - logger.error(f"KeyError: {e}") - dicto = v - placeholder[k] = dicto - placeholder['sheet'] = mp_info['sheet'] - yield placeholder - - def write_reagents(self) -> Workbook: - """ - Performs write operations - - Returns: - Workbook: Workbook with reagents written - """ - for reagent in self.reagents: - sheet = self.xl[reagent['sheet']] - for v in reagent.values(): - if not isinstance(v, dict): - continue - match v['value']: - case datetime(): - v['value'] = v['value'].date() - case _: - pass - sheet.cell(row=v['row'], column=v['column'], value=v['value']) - return self.xl - - -class SampleWriter(object): - """ - object to write sample data into excel file - """ - - def __init__(self, xl: Workbook, submission_type: SubmissionType | str, sample_list: list): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - sample_list (list): List of sample dictionaries to be written to excel file. - """ - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type - self.xl = xl - self.sample_map = submission_type.sample_map['lookup_table'] - # NOTE: exclude any sample without a procedure 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() - - def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]: - """ - Merge sample info with locations - - Args: - sample_list (list): List of sample information - - Returns: - List[dict]: List of merged dictionaries - """ - multiples = ['row', 'column', 'assoc_id', 'submission_rank'] - for sample in sample_list: - sample = self.submission_type.submission_class.custom_sample_writer(sample) - for assoc in zip(sample['row'], sample['column'], sample['submission_rank']): - new = dict(row=assoc[0], column=assoc[1], submission_rank=assoc[2]) - for k, v in sample.items(): - if k in multiples: - continue - new[k] = v - yield new - - def blank_lookup_table(self): - """ - Blanks out columns in the lookup table to ensure help values are removed before writing. - """ - sheet = self.xl[self.sample_map['sheet']] - for row in range(self.sample_map['start_row'], self.sample_map['end_row'] + 1): - for column in self.sample_map['sample_columns'].values(): - if sheet.cell(row, column).data_type != 'f': - sheet.cell(row=row, column=column, value="") - - def write_samples(self) -> Workbook: - """ - Performs writing operations. - - Returns: - Workbook: Workbook with sample written - """ - sheet = self.xl[self.sample_map['sheet']] - columns = self.sample_map['sample_columns'] - for sample in self.samples: - row = self.sample_map['start_row'] + (sample['submission_rank'] - 1) - for k, v in sample.items(): - if isinstance(v, dict): - try: - v = v['value'] - except KeyError: - logger.error(f"Cant convert {v} to single string.") - try: - column = columns[k] - except KeyError: - continue - sheet.cell(row=row, column=column, value=v) - return self.xl - - -class EquipmentWriter(object): - """ - object to write equipment data into excel file - """ - - def __init__(self, xl: Workbook, submission_type: SubmissionType | str, equipment_list: list): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - equipment_list (list): List of equipment dictionaries to write to excel file. - """ - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type - self.xl = xl - equipment_map = {k: v for k, v in self.submission_type.construct_field_map("equipment")} - self.equipment = self.reconcile_map(equipment_list=equipment_list, equipment_map=equipment_map) - - def reconcile_map(self, equipment_list: list, equipment_map: dict) -> Generator[dict, None, None]: - """ - Merges equipment with location data - - Args: - equipment_list (list): List of equipment - equipment_map (dict): Dictionary of equipment locations - - Returns: - List[dict]: List of merged dictionaries - """ - if equipment_list is None: - return - for ii, equipment in enumerate(equipment_list, start=1): - try: - mp_info = equipment_map[equipment['reagentrole']] - except KeyError: - logger.error(f"No {equipment['reagentrole']} in {pformat(equipment_map)}") - mp_info = None - placeholder = copy(equipment) - if not mp_info: - for jj, (k, v) in enumerate(equipment.items(), start=1): - dicto = dict(value=v, row=ii, column=jj) - placeholder[k] = dicto - else: - for jj, (k, v) in enumerate(equipment.items(), start=1): - try: - dicto = dict(value=v, row=mp_info[k]['row'], column=mp_info[k]['column']) - except KeyError as e: - continue - placeholder[k] = dicto - if "asset_number" not in mp_info.keys(): - placeholder['name']['value'] = f"{equipment['name']} - {equipment['asset_number']}" - try: - placeholder['sheet'] = mp_info['sheet'] - except KeyError: - placeholder['sheet'] = "Equipment" - yield placeholder - - def write_equipment(self) -> Workbook: - """ - Performs write operations - - Returns: - Workbook: Workbook with equipment written - """ - for equipment in self.equipment: - if not equipment['sheet'] in self.xl.sheetnames: - self.xl.create_sheet("Equipment") - sheet = self.xl[equipment['sheet']] - for k, v in equipment.items(): - if not isinstance(v, dict): - continue - if isinstance(v['value'], list): - v['value'] = v['value'][0] - try: - sheet.cell(row=v['row'], column=v['column'], value=v['value']) - except AttributeError as e: - logger.error(f"Couldn't write to {equipment['sheet']}, row: {v['row']}, column: {v['column']}") - logger.error(e) - return self.xl - - -class TipWriter(object): - """ - object to write tips data into excel file - """ - - def __init__(self, xl: Workbook, submission_type: SubmissionType | str, tips_list: list): - """ - Args: - xl (Workbook): Openpyxl workbook from submitted excel file. - submission_type (SubmissionType | str): Type of procedure expected (Wastewater, Bacterial Culture, etc.) - tips_list (list): List of tip dictionaries to write to the excel file. - """ - if isinstance(submission_type, str): - submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type - self.xl = xl - tips_map = {k: v for k, v in self.submission_type.construct_field_map("tip")} - self.tips = self.reconcile_map(tips_list=tips_list, tips_map=tips_map) - - def reconcile_map(self, tips_list: List[dict], tips_map: dict) -> Generator[dict, None, None]: - """ - Merges tips with location data - - Args: - tips_list (List[dict]): List of tips - tips_map (dict): Tips locations - - Returns: - List[dict]: List of merged dictionaries - """ - if tips_list is None: - return - for ii, tips in enumerate(tips_list, start=1): - mp_info = tips_map[tips.role] - placeholder = {} - if mp_info == {}: - for jj, (k, v) in enumerate(tips.__dict__.items(), start=1): - dicto = dict(value=v, row=ii, column=jj) - placeholder[k] = dicto - else: - for jj, (k, v) in enumerate(tips.__dict__.items(), start=1): - try: - dicto = dict(value=v, row=mp_info[k]['row'], column=mp_info[k]['column']) - except KeyError as e: - continue - placeholder[k] = dicto - try: - placeholder['sheet'] = mp_info['sheet'] - except KeyError: - placeholder['sheet'] = "Tips" - yield placeholder - - def write_tips(self) -> Workbook: - """ - Performs write operations - - Returns: - Workbook: Workbook with tips written - """ - for tips in self.tips: - if not tips['sheet'] in self.xl.sheetnames: - self.xl.create_sheet("Tips") - sheet = self.xl[tips['sheet']] - for k, v in tips.items(): - if not isinstance(v, dict): - continue - if isinstance(v['value'], list): - v['value'] = v['value'][0] - try: - sheet.cell(row=v['row'], column=v['column'], value=v['value']) - except AttributeError as e: - logger.error(f"Couldn't write to {tips['sheet']}, row: {v['row']}, column: {v['column']}") - logger.error(e) - return self.xl diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index 7322732..5b33b58 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -47,7 +47,7 @@ class ClientSubmissionNamer(DefaultNamer): sub_type = self.get_subtype_from_regex() if not sub_type: logger.warning(f"Getting submissiontype from regex failed, using default submissiontype.") - sub_type = SubmissionType.query(name="Test") + sub_type = SubmissionType.query(name="Default") logger.debug(f"Submission Type: {sub_type}") sys.exit() return sub_type diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index cbfaa2a..be80510 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -250,7 +250,7 @@ class PydReagent(PydBaseClass): report = Report() if self.model_extra is not None: self.__dict__.update(self.model_extra) - reagent, new = Reagent.query_or_create(lot=self.lot, name=self.name) + reagent, new = ReagentLot.query_or_create(lot=self.lot, name=self.name) if new: reagentrole = ReagentRole.query(name=self.reagentrole) reagent.reagentrole = reagentrole @@ -374,7 +374,7 @@ class PydEquipment(PydBaseClass): name: str nickname: str | None # process: List[dict] | None - process: PydProcess | None + process: List[PydProcess] | PydProcess | None equipmentrole: str | PydEquipmentRole | None tips: List[PydTips] | PydTips | None = Field(default=[]) @@ -402,7 +402,7 @@ class PydEquipment(PydBaseClass): value = convert_nans_to_nones(value) if not value: value = [''] - logger.debug(value) + # logger.debug(value) try: # value = [item.strip() for item in value] value = next((PydProcess(**process.details_dict()) for process in value)) @@ -435,7 +435,7 @@ class PydEquipment(PydBaseClass): return value @report_result - def to_sql(self, procedure: Procedure | str = None, kittype: KitType | str = None) -> Tuple[ + def to_sql(self, procedure: Procedure | str = None, proceduretype: ProcedureType | str = None) -> Tuple[ Equipment, ProcedureEquipmentAssociation]: """ Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment @@ -449,8 +449,8 @@ class PydEquipment(PydBaseClass): report = Report() if isinstance(procedure, str): procedure = Procedure.query(name=procedure) - if isinstance(kittype, str): - kittype = KitType.query(name=kittype) + if isinstance(proceduretype, str): + proceduretype = ProcedureType.query(name=proceduretype) logger.debug(f"Querying equipment: {self.asset_number}") equipment = Equipment.query(asset_number=self.asset_number) if equipment is None: @@ -470,8 +470,7 @@ class PydEquipment(PydBaseClass): # NOTE: It looks like the way fetching the process 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 kittype involved. if len(self.processes) > 1: - process = Process.query(proceduretype=procedure.get_submission_type(), kittype=kittype, - equipmentrole=self.role) + process = Process.query(proceduretype=procedure.get_submission_type(), equipmentrole=self.role) else: process = Process.query(name=self.processes[0]) if process is None: @@ -879,15 +878,15 @@ class PydRun(PydBaseClass): #, extra='allow'): pass return SubmissionFormWidget(parent=parent, pyd=self, disable=disable) - def to_writer(self) -> "SheetWriter": - """ - Sends data here to the sheet writer. - - Returns: - SheetWriter: Sheetwriter object that will perform writing. - """ - from backend.excel.writer import SheetWriter - return SheetWriter(self) + # def to_writer(self) -> "SheetWriter": + # """ + # Sends data here to the sheet writer. + # + # Returns: + # SheetWriter: Sheetwriter object that will perform writing. + # """ + # from backend.excel.writer import SheetWriter + # return SheetWriter(self) def construct_filename(self) -> str: """ @@ -1166,10 +1165,10 @@ class PydProcess(PydBaseClass, extra="allow"): proceduretype: List[str] equipment: List[str] equipmentrole: List[str] - kittype: List[str] + # kittype: List[str] tiprole: List[str] - @field_validator("proceduretype", "equipment", "equipmentrole", "kittype", "tiprole", mode="before") + @field_validator("proceduretype", "equipment", "equipmentrole", "tiprole", mode="before") @classmethod def enforce_list(cls, value): if not isinstance(value, list): @@ -1249,8 +1248,8 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): technician: dict = Field(default=dict(value="NA", missing=True)) repeat: bool = Field(default=False) repeat_of: str | None = Field(default=None) - kittype: dict = Field(default=dict(value="NA", missing=True)) - possible_kits: list | None = Field(default=[], validate_default=True) + # kittype: dict = Field(default=dict(value="NA", missing=True)) + # possible_kits: list | None = Field(default=[], validate_default=True) plate_map: str | None = Field(default=None) reagent: list | None = Field(default=[]) reagentrole: dict | None = Field(default={}, validate_default=True) @@ -1258,7 +1257,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): equipment: List[PydEquipment] = Field(default=[]) result: List[PydResults] | List[dict] = Field(default=[]) - @field_validator("name", "technician", "kittype", mode="before") + @field_validator("name", "technician", mode="before")#"kittype", mode="before") @classmethod def convert_to_dict(cls, value): if not value: @@ -1295,18 +1294,18 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): value['missing'] = True return value - @field_validator("possible_kits") - @classmethod - def rescue_possible_kits(cls, value, values): - if not value: - try: - if values.data['proceduretype']: - value = [kittype.__dict__['name'] for kittype in values.data['proceduretype'].kittype] - except KeyError: - pass - return value + # @field_validator("possible_kits") + # @classmethod + # def rescue_possible_kits(cls, value, values): + # if not value: + # try: + # if values.data['proceduretype']: + # value = [kittype.__dict__['name'] for kittype in values.data['proceduretype'].kittype] + # except KeyError: + # pass + # return value - @field_validator("name", "technician", "kittype") + @field_validator("name", "technician")#, "kittype") @classmethod def set_colour(cls, value): try: @@ -1321,20 +1320,26 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): @field_validator("reagentrole") @classmethod def rescue_reagentrole(cls, value, values): + # if not value: + # match values.data['kittype']: + # case dict(): + # if "value" in values.data['kittype'].keys(): + # roi = values.data['kittype']['value'] + # elif "name" in values.data['kittype'].keys(): + # roi = values.data['kittype']['name'] + # else: + # raise KeyError(f"Couldn't find kittype name in the dictionary: {values.data['kittype']}") + # case str(): + # roi = values.data['kittype'] + # if roi != cls.model_fields['kittype'].default['value']: + # kittype = KitType.query(name=roi) + # value = {item.name: item.reagent for item in kittype.reagentrole} if not value: - match values.data['kittype']: - case dict(): - if "value" in values.data['kittype'].keys(): - roi = values.data['kittype']['value'] - elif "name" in values.data['kittype'].keys(): - roi = values.data['kittype']['name'] - else: - raise KeyError(f"Couldn't find kittype name in the dictionary: {values.data['kittype']}") - case str(): - roi = values.data['kittype'] - if roi != cls.model_fields['kittype'].default['value']: - kittype = KitType.query(name=roi) - value = {item.name: item.reagent for item in kittype.reagentrole} + value = {} + for reagentrole in values.data['proceduretype'].reagentrole: + reagents = [reagent.lot_dicts for reagent in reagentrole.reagent] + value[reagentrole.name] = flatten_list(reagents) + # value = {item.name: item.reagent for item in values.data['proceduretype'].reagentrole} return value @field_validator("run") @@ -1416,12 +1421,12 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype))) def update_samples(self, sample_list: List[dict]): - logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}") + # logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}") for iii, sample_dict in enumerate(sample_list, start=1): if sample_dict['sample_id'].startswith("blank_"): sample_dict['sample_id'] = "" row, column = self.proceduretype.ranked_plate[sample_dict['index']] - logger.debug(f"Row: {row}, Column: {column}") + # logger.debug(f"Row: {row}, Column: {column}") try: sample = next( (item for item in self.sample if item.sample_id.upper() == sample_dict['sample_id'].upper())) @@ -1440,7 +1445,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): sample.row = row sample.column = column sample.procedure_rank = sample_dict['index'] - logger.debug(f"Sample of interest: {sample.improved_dict()}") + # logger.debug(f"Sample of interest: {sample.improved_dict()}") # logger.debug(f"Updated samples:\n{pformat(self.sample)}") def update_reagents(self, reagentrole: str, name: str, lot: str, expiry: str): @@ -1456,7 +1461,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): idx = 0 insertable = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) self.reagent.insert(idx, insertable) - logger.debug(self.reagent) + # logger.debug(self.reagent) @classmethod def update_new_reagents(cls, reagent: PydReagent): @@ -1492,24 +1497,24 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): if self.proceduretype: sql.proceduretype = self.proceduretype # Note: convert any new reagents to sql and save - for reagentrole, reagents in self.reagentrole.items(): - for reagent in reagents: - if not reagent.lot or reagent.name == "--New--": - continue - self.update_new_reagents(reagent) + # for reagentrole, reagents in self.reagentrole.items(): + for reagent in self.reagent: + if not reagent.lot or reagent.name == "--New--": + continue + self.update_new_reagents(reagent) # NOTE: reset reagent associations. sql.procedurereagentassociation = [] for reagent in self.reagent: if isinstance(reagent, dict): reagent = PydReagent(**reagent) - # logger.debug(reagent) + logger.debug(reagent) reagentrole = reagent.reagentrole reagent = reagent.to_sql() # logger.debug(reagentrole) - if reagent not in sql.reagent: + if reagent not in sql.reagentlot: # NOTE: Remove any previous association for this role. if sql.id: - removable = ProcedureReagentAssociation.query(procedure=sql, reagentrole=reagentrole) + removable = ProcedureReagentLotAssociation.query(procedure=sql, reagentrole=reagentrole) else: removable = [] logger.debug(f"Removable: {removable}") @@ -1520,7 +1525,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): else: removable.delete() # logger.debug(f"Adding {reagent} to {sql}") - reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql, reagentrole=reagentrole) + reagent_assoc = ProcedureReagentLotAssociation(reagentlot=reagent, procedure=sql, reagentrole=reagentrole) try: start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 except ValueError: @@ -1543,10 +1548,6 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql, row=sample.row, column=sample.column, procedure_rank=sample.procedure_rank) - if self.kittype['value'] not in ["NA", None, ""]: - kittype = KitType.query(name=self.kittype['value'], limit=1) - if kittype: - sql.kittype = kittype for equipment in self.equipment: equip = Equipment.query(name=equipment.name) if equip not in sql.equipment: @@ -1729,7 +1730,7 @@ class PydClientSubmission(PydBaseClass): case SubmissionType(): pass case _: - sql.submissiontype = SubmissionType.query(name="Test") + sql.submissiontype = SubmissionType.query(name="Default") for k in list(self.model_fields.keys()) + list(self.model_extra.keys()): logger.debug(f"Running {k}") attribute = getattr(self, k) diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index b267b89..b4e5705 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.db.models import Reagent, KitType +from backend.db.models import Reagent from tools import ( check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, under_development diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index c3663e7..afd9ffa 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -6,7 +6,7 @@ from PyQt6.QtWidgets import ( QWidget, QComboBox, QPushButton ) from PyQt6.QtCore import QSignalBlocker -from backend import ChartReportMaker +from backend.excel.reports import ChartReportMaker from backend.db import ControlType import logging from tools import Report, report_result diff --git a/src/submissions/frontend/widgets/date_type_picker.py b/src/submissions/frontend/widgets/date_type_picker.py index ff7c8c7..604e1de 100644 --- a/src/submissions/frontend/widgets/date_type_picker.py +++ b/src/submissions/frontend/widgets/date_type_picker.py @@ -2,7 +2,7 @@ from PyQt6.QtWidgets import ( QVBoxLayout, QDialog, QDialogButtonBox ) from .misc import CheckableComboBox, StartEndDatePicker -from backend.db.models.kits import SubmissionType +from backend.db.models.procedures import SubmissionType class DateTypePicker(QDialog): diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index 66e62ab..919f48e 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -21,7 +21,7 @@ class EquipmentUsage(QDialog): self.procedure = procedure self.setWindowTitle(f"Equipment Checklist - {procedure.name}") self.used_equipment = self.procedure.equipment - self.kit = self.procedure.kittype + # self.kit = self.procedure.kittype self.opt_equipment = procedure.proceduretype.get_equipment() self.layout = QVBoxLayout() self.setLayout(self.layout) diff --git a/src/submissions/frontend/widgets/equipment_usage_2.py b/src/submissions/frontend/widgets/equipment_usage_2.py index 42974fc..3749cf6 100644 --- a/src/submissions/frontend/widgets/equipment_usage_2.py +++ b/src/submissions/frontend/widgets/equipment_usage_2.py @@ -57,7 +57,7 @@ class EquipmentUsage(QDialog): proceduretype = procedure.proceduretype proceduretype_dict = proceduretype.details_dict() run = procedure.run - proceduretype_dict['equipment_json'] = flatten_list([item['equipment_json'] for item in proceduretype_dict['equipment']]) + # proceduretype_dict['equipment_json'] = flatten_list([item['equipment_json'] for item in proceduretype_dict['equipment']]) # proceduretype_dict['equipment_json'] = [ # {'name': 'Liquid Handler', 'equipment': [ # {'name': 'Other', 'asset_number': 'XXX', 'processes': [ diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index 174c378..7f7355d 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any, List if TYPE_CHECKING: from backend.db.models import Run, Procedure from backend.validators import PydProcedure -from tools import jinja_template_loading, get_application_from_parent, render_details_template +from tools import jinja_template_loading, get_application_from_parent, render_details_template, sanitize_object_for_json logger = logging.getLogger(f"submissions.{__name__}") @@ -32,10 +32,11 @@ class ProcedureCreation(QDialog): self.edit = edit self.run = procedure.run self.procedure = procedure + logger.debug(f"procedure: {pformat(self.procedure.__dict__)}") self.proceduretype = procedure.proceduretype self.setWindowTitle(f"New {self.proceduretype.name} for {self.run.rsl_plate_number}") # self.created_procedure = self.proceduretype.construct_dummy_procedure(run=self.run) - self.procedure.update_kittype_reagentroles(kittype=self.procedure.possible_kits[0]) + # self.procedure.update_kittype_reagentroles(kittype=self.procedure.possible_kits[0]) # self.created_procedure.samples = self.run.constuct_sample_dicts_for_proceduretype(proceduretype=self.proceduretype) # logger.debug(f"Samples to map\n{pformat(self.created_procedure.samples)}") @@ -70,8 +71,13 @@ class ProcedureCreation(QDialog): def set_html(self): from .equipment_usage_2 import EquipmentUsage - logger.debug(f"Edit: {self.edit}") + # logger.debug(f"Edit: {self.edit}") proceduretype_dict = self.proceduretype.details_dict() + logger.debug(f"Reagent roles: {self.procedure.reagentrole}") + logger.debug(f"Equipment roles: {pformat(proceduretype_dict['equipment'])}") + # NOTE: Add --New-- as an option for reagents. + for key, value in self.procedure.reagentrole.items(): + value.append(dict(name="--New--")) if self.procedure.equipment: for equipmentrole in proceduretype_dict['equipment']: # NOTE: Check if procedure equipment is present and move to head of the list if so. @@ -85,6 +91,8 @@ class ProcedureCreation(QDialog): equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop( equipmentrole['equipment'].index(item_in_er_list))) proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True) + proceduretype_dict['equipment'] = [sanitize_object_for_json(object) for object in proceduretype_dict['equipment']] + self.update_equipment = EquipmentUsage.update_equipment regex = re.compile(r".*R\d$") proceduretype_dict['previous'] = [""] + [item.name for item in self.run.procedure if item.proceduretype == self.proceduretype and not bool(regex.match(item.name))] @@ -94,12 +102,13 @@ class ProcedureCreation(QDialog): js_in=["procedure_form", "grid_drag", "context_menu"], proceduretype=proceduretype_dict, run=self.run.details_dict(), - procedure=self.procedure.__dict__, + # procedure=self.procedure.__dict__, + procedure=self.procedure, plate_map=self.plate_map, edit=self.edit ) - # with open("procedure_creation_rendered.html", "w") as f: - # f.write(html) + with open("procedure_creation_rendered.html", "w") as f: + f.write(html) self.webview.setHtml(html) @pyqtSlot(str, str, str, str) @@ -119,11 +128,13 @@ class ProcedureCreation(QDialog): eoi.name = equipment.name eoi.asset_number = equipment.asset_number eoi.nickname = equipment.nickname - process = next((prcss for prcss in equipment.process if prcss.name == process)) - eoi.process = process.to_pydantic() - tips = next((tps for tps in equipment.tips if tps.name == tips)) - eoi.tips = tips.to_pydantic() - self.procedure.equipment.append(eoi) + process = next((prcss for prcss in equipment.process if prcss.name == process), None) + if process: + eoi.process = process.to_pydantic() + tips = next((tps for tps in equipment.tips if tps.name == tips), None) + if tips: + eoi.tips = tips.to_pydantic() + self.procedure.equipment.append(eoi) logger.debug(f"Updated equipment: {self.procedure.equipment}") @pyqtSlot(str, str) diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index bb45996..8c080bc 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -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 Run, Sample, Reagent, KitType, Equipment, Process, Tips +from backend.db.models import Run, Sample, Reagent, ProcedureType, Equipment, Process, Tips from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent, list_str_comparator from .functions import select_save_file, save_pdf from pathlib import Path @@ -161,7 +161,7 @@ class SubmissionDetails(QDialog): self.setWindowTitle(f"Sample Details - {sample.sample_id}") @pyqtSlot(str, str) - def reagent_details(self, reagent: str | Reagent, kit: str | KitType): + def reagent_details(self, reagent: str | Reagent, proceduretype: str | ProcedureType): """ Changes details view to summary of Reagent @@ -172,9 +172,9 @@ class SubmissionDetails(QDialog): logger.debug(f"Reagent details.") if isinstance(reagent, str): reagent = Reagent.query(lot=reagent) - if isinstance(kit, str): - self.kit = KitType.query(name=kit) - base_dict = reagent.to_sub_dict(kittype=self.kit, full_data=True) + if isinstance(proceduretype, str): + self.proceduretype = ProcedureType.query(name=proceduretype) + base_dict = reagent.to_sub_dict(proceduretype=self.proceduretype, full_data=True) env = jinja_template_loading() temp_name = "reagent_details.html" try: diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 04d6060..37c19f5 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -15,7 +15,7 @@ from tools import Report, Result, check_not_nan, main_form_style, report_result, from backend.validators import PydReagent, PydClientSubmission, PydSample from backend.db import ( ClientLab, SubmissionType, Reagent, - ReagentRole, KitTypeReagentRoleAssociation, Run, ClientSubmission + ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission ) from pprint import pformat from .pop_ups import QuestionAsker, AlertPop @@ -760,8 +760,8 @@ class SubmissionFormWidget(QWidget): def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None: super().__init__(scrollWidget=scrollWidget) self.setEditable(True) - looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.equipmentrole, - kittype=extraction_kit) + looked_up_rt = ProcedureTypeReagentRoleAssociation.query(reagentrole=reagent.reagentrole, + proceduretype=extraction_kit) relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()] # NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows if str(reagent.lot) not in relevant_reagents: diff --git a/src/submissions/frontend/widgets/summary.py b/src/submissions/frontend/widgets/summary.py index adaec8b..f546f52 100644 --- a/src/submissions/frontend/widgets/summary.py +++ b/src/submissions/frontend/widgets/summary.py @@ -4,7 +4,7 @@ Pane to hold information e.g. cost summary. from .info_tab import InfoPane from PyQt6.QtWidgets import QWidget, QLabel, QPushButton from backend.db import ClientLab -from backend.excel import ReportMaker +from backend.excel.reports import ReportMaker from .misc import CheckableComboBox import logging diff --git a/src/submissions/templates/js/procedure_form.js b/src/submissions/templates/js/procedure_form.js index 05f631b..627d56b 100644 --- a/src/submissions/templates/js/procedure_form.js +++ b/src/submissions/templates/js/procedure_form.js @@ -1,9 +1,7 @@ -document.getElementById("kittype").addEventListener("change", function() { - backend.update_kit(this.value); -}) + var formchecks = document.getElementsByClassName('form_check'); @@ -46,7 +44,7 @@ var reagentRoles = document.getElementsByClassName("reagentrole"); for(let i = 0; i < reagentRoles.length; i++) { reagentRoles[i].addEventListener("change", function() { if (reagentRoles[i].value.includes("--New--")) { - alert("Create new reagent.") +// alert("Create new reagent.") var br = document.createElement("br"); var new_reg = document.getElementById("new_" + reagentRoles[i].id); var new_form = document.createElement("form"); diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index 105f10d..988f4da 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -29,12 +29,7 @@ {% endfor %}

-
-
+ {% if procedure['reagentrole'] %}

Reagents

@@ -47,6 +42,7 @@ {% endfor %}
+
{% endfor %} {% endif %} @@ -58,9 +54,10 @@ {% endif %} - {% if proceduretype['equipment_section'] %} - {{ proceduretype['equipment_section'] }} - {% endif %} + {% with equipmentroles=proceduretype['equipment'], child=True %} + {% include 'support/equipment_usage.html' %} + {% endwith %} + {% include 'support/context_menu.html' %} {% endblock %} diff --git a/src/submissions/templates/support/equipment_usage.html b/src/submissions/templates/support/equipment_usage.html index f97205d..a037393 100644 --- a/src/submissions/templates/support/equipment_usage.html +++ b/src/submissions/templates/support/equipment_usage.html @@ -19,7 +19,7 @@

Equipment




-{% for equipmentrole in proceduretype['equipment_json'] %} +{% for equipmentrole in equipmentroles %}

@@ -38,6 +38,7 @@
+
{% endfor %} {% if not child %} @@ -48,7 +49,7 @@ {% endfor %} {% endif %}