diff --git a/alembic.ini b/alembic.ini index 0cddafa..bbda290 100644 --- a/alembic.ini +++ b/alembic.ini @@ -56,9 +56,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # output_encoding = utf-8 ; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db -; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-demo.db +sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-demo.db ;sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-prototypes.db -sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\mytests\test_assets\submissions-test.db +; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\mytests\test_assets\submissions-test.db [post_write_hooks] diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 9a1b4de..0b08803 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -19,11 +19,11 @@ from io import BytesIO logger = logging.getLogger(f'submissions.{__name__}') # logger.debug("Table for ReagentType/Reagent relations") -reagenttypes_reagents = Table( - "_reagenttypes_reagents", +reagentroles_reagents = Table( + "_reagentroles_reagents", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagent.id")), - Column("reagenttype_id", INTEGER, ForeignKey("_reagenttype.id")), + Column("reagentrole_id", INTEGER, ForeignKey("_reagentrole.id")), extend_existing=True ) @@ -84,16 +84,16 @@ class KitType(BaseClass): processes = relationship("Process", back_populates="kit_types", secondary=kittypes_processes) #: equipment processes used by this kit - kit_reagenttype_associations = relationship( - "KitTypeReagentTypeAssociation", + kit_reagentrole_associations = relationship( + "KitTypeReagentRoleAssociation", back_populates="kit_type", cascade="all, delete-orphan", ) # creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 - reagent_types = association_proxy("kit_reagenttype_associations", "reagent_type", - creator=lambda RT: KitTypeReagentTypeAssociation( - reagent_type=RT)) #: Association proxy to KitTypeReagentTypeAssociation + reagent_roles = association_proxy("kit_reagentrole_associations", "reagent_role", + creator=lambda RT: KitTypeReagentRoleAssociation( + reagent_role=RT)) #: Association proxy to KitTypeReagentRoleAssociation kit_submissiontype_associations = relationship( "SubmissionTypeKitTypeAssociation", @@ -111,7 +111,7 @@ class KitType(BaseClass): """ return f"" - def get_reagents(self, required: bool = False, submission_type: str | SubmissionType | None = None) -> List[ReagentType]: + def get_reagents(self, required: bool = False, submission_type: str | SubmissionType | None = None) -> List[ReagentRole]: """ Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation. @@ -120,25 +120,25 @@ class KitType(BaseClass): submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None. Returns: - List[ReagentType]: List of reagents linked to this kit. + List[ReagentRole]: List of reagents linked to this kit. """ match submission_type: case SubmissionType(): # logger.debug(f"Getting reagents by SubmissionType {submission_type}") - relevant_associations = [item for item in self.kit_reagenttype_associations if + relevant_associations = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type] case str(): # logger.debug(f"Getting reagents by str {submission_type}") - relevant_associations = [item for item in self.kit_reagenttype_associations if + relevant_associations = [item for item in self.kit_reagentrole_associations if item.submission_type.name == submission_type] case _: # logger.debug(f"Getting reagents") - relevant_associations = [item for item in self.kit_reagenttype_associations] + relevant_associations = [item for item in self.kit_reagentrole_associations] if required: # logger.debug(f"Filtering by required.") - return [item.reagent_type for item in relevant_associations if item.required == 1] + return [item.reagent_role for item in relevant_associations if item.required == 1] else: - return [item.reagent_type for item in relevant_associations] + return [item.reagent_role for item in relevant_associations] # TODO: Move to BasicSubmission? def construct_xl_map_for_use(self, submission_type: str | SubmissionType) -> dict: @@ -156,17 +156,17 @@ class KitType(BaseClass): match submission_type: case str(): # logger.debug(f"Constructing xl map with str {submission_type}") - assocs = [item for item in self.kit_reagenttype_associations if + assocs = [item for item in self.kit_reagentrole_associations if item.submission_type.name == submission_type] case SubmissionType(): # logger.debug(f"Constructing xl map with SubmissionType {submission_type}") - assocs = [item for item in self.kit_reagenttype_associations if item.submission_type == submission_type] + assocs = [item for item in self.kit_reagentrole_associations if item.submission_type == submission_type] case _: raise ValueError(f"Wrong variable type: {type(submission_type)} used!") # logger.debug("Get all KitTypeReagentTypeAssociation for SubmissionType") for assoc in assocs: try: - info_map[assoc.reagent_type.name] = assoc.uses + info_map[assoc.reagent_role.name] = assoc.uses except TypeError: continue return info_map @@ -226,34 +226,34 @@ class KitType(BaseClass): super().save() -class ReagentType(BaseClass): +class ReagentRole(BaseClass): """ Base of reagent type abstract """ id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(64)) #: name of reagent type - instances = relationship("Reagent", back_populates="type", - secondary=reagenttypes_reagents) #: concrete instances of this reagent type + instances = relationship("Reagent", back_populates="role", + secondary=reagentroles_reagents) #: concrete instances of this reagent type eol_ext = Column(Interval()) #: extension of life interval - reagenttype_kit_associations = relationship( - "KitTypeReagentTypeAssociation", - back_populates="reagent_type", + reagentrole_kit_associations = relationship( + "KitTypeReagentRoleAssociation", + back_populates="reagent_role", cascade="all, delete-orphan", ) #: Relation to KitTypeReagentTypeAssociation # creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 - kit_types = association_proxy("reagenttype_kit_associations", "kit_type", - creator=lambda kit: KitTypeReagentTypeAssociation( - kit_type=kit)) #: Association proxy to KitTypeReagentTypeAssociation + kit_types = association_proxy("reagentrole_kit_associations", "kit_type", + creator=lambda kit: KitTypeReagentRoleAssociation( + kit_type=kit)) #: Association proxy to KitTypeReagentRoleAssociation def __repr__(self) -> str: """ Returns: str: Representation of object """ - return f"" + return f"" @classmethod @setup_lookup @@ -262,7 +262,7 @@ class ReagentType(BaseClass): kit_type: KitType | str | None = None, reagent: Reagent | str | None = None, limit: int = 0, - ) -> ReagentType | List[ReagentType]: + ) -> ReagentRole | List[ReagentRole]: """ Lookup reagent types in the database. @@ -276,7 +276,7 @@ class ReagentType(BaseClass): ValueError: Raised if only kit_type or reagent, not both, given. Returns: - ReagentType|List[ReagentType]: ReagentType or list of ReagentTypes matching filter. + ReagentRole|List[ReagentRole]: ReagentRole or list of ReagentRoles matching filter. """ query: Query = cls.__database_session__.query(cls) if (kit_type is not None and reagent is None) or (reagent is not None and kit_type is None): @@ -296,10 +296,10 @@ class ReagentType(BaseClass): reagent = Reagent.query(lot_number=reagent) case _: pass - assert reagent.type + assert reagent.role # logger.debug(f"Looking up reagent type for {type(kit_type)} {kit_type} and {type(reagent)} {reagent}") # logger.debug(f"Kit reagent types: {kit_type.reagent_types}") - result = list(set(kit_type.reagent_types).intersection(reagent.type)) + result = list(set(kit_type.reagent_roles).intersection(reagent.role)) # logger.debug(f"Result: {result}") try: return result[0] @@ -322,7 +322,7 @@ class ReagentType(BaseClass): PydReagent: PydReagent representation of this object. """ from backend.validators.pydant import PydReagent - return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today()) + return PydReagent(lot=None, role=self.name, name=self.name, expiry=date.today()) @check_authorization def save(self): @@ -335,10 +335,10 @@ class Reagent(BaseClass): """ id = Column(INTEGER, primary_key=True) #: primary key - type = relationship("ReagentType", back_populates="instances", - secondary=reagenttypes_reagents) #: joined parent reagent type - type_id = Column(INTEGER, ForeignKey("_reagenttype.id", ondelete='SET NULL', - name="fk_reagent_type_id")) #: id of parent reagent type + role = relationship("ReagentRole", back_populates="instances", + secondary=reagentroles_reagents) #: joined parent reagent type + role_id = Column(INTEGER, ForeignKey("_reagentrole.id", ondelete='SET NULL', + name="fk_reagent_role_id")) #: id of parent reagent type 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 @@ -356,7 +356,7 @@ class Reagent(BaseClass): if self.name is not None: return f"" else: - return f"" + return f"" def to_sub_dict(self, extraction_kit: KitType = None) -> dict: """ @@ -371,12 +371,12 @@ class Reagent(BaseClass): if extraction_kit is not None: # NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType try: - reagent_role = list(set(self.type).intersection(extraction_kit.reagent_types))[0] + reagent_role = list(set(self.role).intersection(extraction_kit.reagent_roles))[0] # NOTE: Most will be able to fall back to first ReagentType in itself because most will only have 1. except: - reagent_role = self.type[0] + reagent_role = self.role[0] else: - reagent_role = self.type[0] + reagent_role = self.role[0] try: rtype = reagent_role.name.replace("_", " ") except AttributeError: @@ -393,7 +393,7 @@ class Reagent(BaseClass): place_holder = place_holder.strftime("%Y-%m-%d") return dict( name=self.name, - type=rtype, + role=rtype, lot=self.lot, expiry=place_holder, missing=False @@ -411,10 +411,10 @@ class Reagent(BaseClass): """ report = Report() # logger.debug(f"Attempting update of last used reagent type at intersection of ({self}), ({kit})") - rt = ReagentType.query(kit_type=kit, reagent=self, limit=1) + rt = ReagentRole.query(kit_type=kit, reagent=self, limit=1) if rt is not None: # logger.debug(f"got reagenttype {rt}") - assoc = KitTypeReagentTypeAssociation.query(kit_type=kit, reagent_type=rt) + assoc = KitTypeReagentRoleAssociation.query(kit_type=kit, reagent_role=rt) if assoc is not None: if assoc.last_used != self.lot: # logger.debug(f"Updating {assoc} last used to {self.lot}") @@ -429,7 +429,7 @@ class Reagent(BaseClass): @setup_lookup def query(cls, id: int | None = None, - reagent_type: str | ReagentType | None = None, + reagent_role: str | ReagentRole | None = None, lot_number: str | None = None, name: str | None = None, limit: int = 0 @@ -438,7 +438,7 @@ class Reagent(BaseClass): Lookup a list of reagents from the database. Args: - reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None. + reagent_role (str | models.ReagentType | None, optional): Reagent type. Defaults to None. lot_number (str | None, optional): Reagent lot number. Defaults to None. name (str | None, optional): Reagent name. Defaults to None. limit (int, optional): limit of results returned. Defaults to 0. @@ -453,13 +453,13 @@ class Reagent(BaseClass): limit = 1 case _: pass - match reagent_type: + match reagent_role: case str(): # logger.debug(f"Looking up reagents by reagent type str: {reagent_type}") - query = query.join(cls.type).filter(ReagentType.name == reagent_type) - case ReagentType(): + query = query.join(cls.role).filter(ReagentRole.name == reagent_role) + case ReagentRole(): # logger.debug(f"Looking up reagents by reagent type ReagentType: {reagent_type}") - query = query.filter(cls.type.contains(reagent_type)) + query = query.filter(cls.role.contains(reagent_role)) case _: pass match name: @@ -582,7 +582,7 @@ class SubmissionType(BaseClass): "equipment_role") #: Proxy of equipmentrole associations submissiontype_kit_rt_associations = relationship( - "KitTypeReagentTypeAssociation", + "KitTypeReagentRoleAssociation", back_populates="submission_type", cascade="all, delete-orphan" ) #: triple association of KitTypes, ReagentTypes, SubmissionTypes @@ -617,7 +617,7 @@ class SubmissionType(BaseClass): if isinstance(filepath, str): filepath = Path(filepath) try: - xl = ExcelFile(filepath) + ExcelFile(filepath) except ValueError: raise ValueError(f"File {filepath} is not of appropriate type.") with open(filepath, "rb") as f: @@ -836,13 +836,13 @@ class SubmissionTypeKitTypeAssociation(BaseClass): return cls.execute_query(query=query, limit=limit) -class KitTypeReagentTypeAssociation(BaseClass): +class KitTypeReagentRoleAssociation(BaseClass): """ table containing reagenttype/kittype associations DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html """ - reagent_types_id = Column(INTEGER, ForeignKey("_reagenttype.id"), + reagent_roles_id = Column(INTEGER, ForeignKey("_reagentrole.id"), primary_key=True) #: id of associated reagent type kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type submission_type_id = Column(INTEGER, ForeignKey("_submissiontype.id"), primary_key=True) @@ -851,23 +851,23 @@ class KitTypeReagentTypeAssociation(BaseClass): last_used = Column(String(32)) #: last used lot number of this type of reagent kit_type = relationship(KitType, - back_populates="kit_reagenttype_associations") #: relationship to associated KitType + back_populates="kit_reagentrole_associations") #: relationship to associated KitType # reference to the "ReagentType" object - reagent_type = relationship(ReagentType, - back_populates="reagenttype_kit_associations") #: relationship to associated ReagentType + reagent_role = relationship(ReagentRole, + back_populates="reagentrole_kit_associations") #: relationship to associated ReagentType submission_type = relationship(SubmissionType, back_populates="submissiontype_kit_rt_associations") #: relationship to associated SubmissionType - def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1): + def __init__(self, kit_type=None, reagent_role=None, uses=None, required=1): self.kit_type = kit_type - self.reagent_type = reagent_type + self.reagent_role = reagent_role self.uses = uses self.required = required def __repr__(self) -> str: - return f"" + return f"" @validates('required') def validate_age(self, key, value): @@ -888,8 +888,8 @@ class KitTypeReagentTypeAssociation(BaseClass): raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') return value - @validates('reagenttype') - def validate_reagenttype(self, key, value): + @validates('reagentrole') + def validate_reagentrole(self, key, value): """ Ensures reagenttype is an actual ReagentType @@ -903,23 +903,23 @@ class KitTypeReagentTypeAssociation(BaseClass): Returns: _type_: ReagentType """ - if not isinstance(value, ReagentType): - raise ValueError(f'{value} is not a reagenttype') + if not isinstance(value, ReagentRole): + raise ValueError(f'{value} is not a reagentrole') return value @classmethod @setup_lookup def query(cls, kit_type: KitType | str | None = None, - reagent_type: ReagentType | str | None = None, + reagent_role: ReagentRole | str | None = None, limit: int = 0 - ) -> KitTypeReagentTypeAssociation | List[KitTypeReagentTypeAssociation]: + ) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]: """ Lookup junction of ReagentType and KitType Args: kit_type (models.KitType | str | None): KitType of interest. - reagent_type (models.ReagentType | str | None): ReagentType of interest. + reagent_role (models.ReagentType | str | None): ReagentType of interest. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. Returns: @@ -935,16 +935,16 @@ class KitTypeReagentTypeAssociation(BaseClass): query = query.join(KitType).filter(KitType.name == kit_type) case _: pass - match reagent_type: - case ReagentType(): + match reagent_role: + case ReagentRole(): # logger.debug(f"Lookup KitTypeReagentTypeAssociation by reagent_type ReagentType {reagent_type}") - query = query.filter(cls.reagent_type == reagent_type) + query = query.filter(cls.reagent_role == reagent_role) case str(): # logger.debug(f"Lookup KitTypeReagentTypeAssociation by reagent_type ReagentType {reagent_type}") - query = query.join(ReagentType).filter(ReagentType.name == reagent_type) + query = query.join(ReagentRole).filter(ReagentRole.name == reagent_role) case _: pass - if kit_type != None and reagent_type != None: + if kit_type != None and reagent_role != None: limit = 1 return cls.execute_query(query=query, limit=limit) @@ -1478,4 +1478,45 @@ class Process(BaseClass): return cls.execute_query(query=query, limit=limit) -# class Tips(Reagent): \ No newline at end of file +# class TipRole(BaseClass): +# +# id = Column(INTEGER, primary_key=True) #: primary key +# name = Column(String(64)) #: name of reagent type +# instances = relationship("Tips", back_populates="role", +# secondary=reagenttypes_reagents) #: concrete instances of this reagent type +# +# tiprole_kit_associations = relationship( +# "KitTypeTipRoleAssociation", +# back_populates="tip_role", +# cascade="all, delete-orphan", +# ) #: Relation to KitTypeReagentTypeAssociation +# +# # creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 +# kit_types = association_proxy("tiprole_kit_associations", "kit_type", +# creator=lambda kit: KitTypeReagentTypeAssociation( +# kit_type=kit)) #: Association proxy to KitTypeReagentTypeAssociation +# +# def __repr__(self): +# return f"" +# +# class Tips(BaseClass): +# +# id = Column(INTEGER, primary_key=True) #: primary key +# role = relationship("TipRole", back_populates="instances", +# secondary=reagenttypes_reagents) #: joined parent reagent type +# role_id = Column(INTEGER, ForeignKey("_tiprole.id", ondelete='SET NULL', +# name="fk_tip_role_id")) #: id of parent reagent type +# name = Column(String(64)) #: tip common name +# lot = Column(String(64)) #: lot number of tips +# +# tips_submission_associations = relationship( +# "SubmissionTipsAssociation", +# back_populates="tips", +# cascade="all, delete-orphan", +# ) #: Relation to SubmissionSampleAssociation +# +# submissions = association_proxy("tips_submission_associations", +# "submission") #: Association proxy to SubmissionSampleAssociation.samples +# +# def __repr__(self): +# return f"" diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index d369af7..63095cf 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -266,9 +266,9 @@ class BasicSubmission(BaseClass): for k in self.extraction_kit.construct_xl_map_for_use(self.submission_type): if k == 'info': continue - if not any([item['type'] == k for item in reagents]): + if not any([item['role'] == k for item in reagents]): reagents.append( - dict(type=k, name="Not Applicable", lot="NA", expiry=date(year=1970, month=1, day=1), + dict(role=k, name="Not Applicable", lot="NA", expiry=date(year=1970, month=1, day=1), missing=True)) except Exception as e: logger.error(f"We got an error retrieving reagents: {e}") diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index acfa67f..fa2a126 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -139,6 +139,7 @@ class SheetParser(object): # logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}") pyd_dict = copy(self.sub) pyd_dict['samples'] = [PydSample(**sample) for sample in self.sub['samples']] + logger.debug(f"Reagents: {pformat(self.sub['reagents'])}") pyd_dict['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']] # logger.debug(f"Equipment: {self.sub['equipment']}") try: @@ -307,7 +308,7 @@ class ReagentParser(object): comment = "" except (KeyError, IndexError): listo.append( - PydReagent(type=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True)) + PydReagent(role=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True)) continue # NOTE: If the cell is blank tell the PydReagent if check_not_nan(lot): @@ -324,7 +325,7 @@ class ReagentParser(object): logger.warning(f"name is not a string.") check = True if check: - listo.append(dict(type=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, + listo.append(dict(role=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, missing=missing)) return listo diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index b4c536c..f9cafb9 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -178,5 +178,5 @@ class RSLNamer(object): return template.render(**kwargs) -from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentType, \ +from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ PydEquipment, PydEquipmentRole diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 0a75902..228298c 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -25,7 +25,7 @@ logger = logging.getLogger(f"submissions.{__name__}") class PydReagent(BaseModel): lot: str | None - type: str | None + role: str | None expiry: date | Literal['NA'] | None name: str | None missing: bool = Field(default=True) @@ -38,7 +38,7 @@ class PydReagent(BaseModel): return "" return value - @field_validator("type", mode='before') + @field_validator("role", mode='before') @classmethod def remove_undesired_types(cls, value): match value: @@ -47,7 +47,7 @@ class PydReagent(BaseModel): case _: return value - @field_validator("type") + @field_validator("role") @classmethod def rescue_type_with_lookup(cls, value, values): if value == None and values.data['lot'] != None: @@ -147,10 +147,10 @@ class PydReagent(BaseModel): match key: case "lot": reagent.lot = value.upper() - case "type": - reagent_type = ReagentType.query(name=value) - if reagent_type != None: - reagent.type.append(reagent_type) + case "role": + reagent_role = ReagentRole.query(name=value) + if reagent_role is not None: + reagent.role.append(reagent_role) case "comment": continue case "expiry": @@ -792,11 +792,11 @@ class PydSubmission(BaseModel, extra='allow'): # logger.debug(f"Kit reagents: {ext_kit_rtypes}") # logger.debug(f"Submission reagents: {self.reagents}") # Exclude any reagenttype found in this pyd not expected in kit. - expected_check = [item.type for item in ext_kit_rtypes] - output_reagents = [rt for rt in self.reagents if rt.type in expected_check] + expected_check = [item.role for item in ext_kit_rtypes] + output_reagents = [rt for rt in self.reagents if rt.role in expected_check] # logger.debug(f"Already have these reagent types: {output_reagents}") - missing_check = [item.type for item in output_reagents] - missing_reagents = [rt for rt in ext_kit_rtypes if rt.type not in missing_check] + missing_check = [item.role for item in output_reagents] + missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check] missing_reagents += [rt for rt in output_reagents if rt.missing] output_reagents += [rt for rt in missing_reagents if rt not in output_reagents] # logger.debug(f"Missing reagents types: {missing_reagents}") @@ -805,7 +805,7 @@ class PydSubmission(BaseModel, extra='allow'): result = None else: result = Result( - msg=f"The excel sheet you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", + msg=f"The excel sheet you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.role.upper() for item in missing_reagents]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning") report.add_result(result) return output_reagents, report @@ -850,7 +850,7 @@ class PydOrganization(BaseModel): return instance -class PydReagentType(BaseModel): +class PydReagentRole(BaseModel): name: str eol_ext: timedelta | int | None uses: dict | None @@ -863,33 +863,33 @@ class PydReagentType(BaseModel): return timedelta(days=value) return value - def toSQL(self, kit: KitType) -> ReagentType: + def toSQL(self, kit: KitType) -> ReagentRole: """ Converts this instance into a backend.db.models.ReagentType instance Args: - kit (KitType): KitType joined to the reagenttype + kit (KitType): KitType joined to the reagentrole Returns: - ReagentType: ReagentType instance + ReagentRole: ReagentType instance """ - instance: ReagentType = ReagentType.query(name=self.name) + instance: ReagentRole = ReagentRole.query(name=self.name) if instance == None: - instance = ReagentType(name=self.name, eol_ext=self.eol_ext) + instance = ReagentRole(name=self.name, eol_ext=self.eol_ext) # logger.debug(f"This is the reagent type instance: {instance.__dict__}") try: - assoc = KitTypeReagentTypeAssociation.query(reagent_type=instance, kit_type=kit) + assoc = KitTypeReagentRoleAssociation.query(reagent_role=instance, kit_type=kit) except StatementError: assoc = None if assoc == None: - assoc = KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=instance, uses=self.uses, + assoc = KitTypeReagentRoleAssociation(kit_type=kit, reagent_role=instance, uses=self.uses, required=self.required) return instance class PydKit(BaseModel): name: str - reagent_types: List[PydReagentType] = [] + reagent_roles: List[PydReagentRole] = [] def toSQL(self) -> Tuple[KitType, Report]: """ @@ -902,7 +902,7 @@ class PydKit(BaseModel): instance = KitType.query(name=self.name) if instance == None: instance = KitType(name=self.name) - [item.toSQL(instance) for item in self.reagent_types] + [item.toSQL(instance) for item in self.reagent_roles] return instance, report diff --git a/src/submissions/frontend/widgets/kit_creator.py b/src/submissions/frontend/widgets/kit_creator.py index 8f43444..4e23455 100644 --- a/src/submissions/frontend/widgets/kit_creator.py +++ b/src/submissions/frontend/widgets/kit_creator.py @@ -5,8 +5,8 @@ from PyQt6.QtWidgets import ( QSpinBox, QDateEdit ) from sqlalchemy import FLOAT, INTEGER -from backend.db import SubmissionTypeKitTypeAssociation, SubmissionType, ReagentType -from backend.validators import PydReagentType, PydKit +from backend.db import SubmissionTypeKitTypeAssociation, SubmissionType, ReagentRole +from backend.validators import PydReagentRole, PydKit import logging from pprint import pformat from tools import Report @@ -81,7 +81,7 @@ class KitAdder(QWidget): """ # get bottommost row maxrow = self.grid.rowCount() - reg_form = ReagentTypeForm(parent=self) + reg_form = ReagentRoleForm(parent=self) reg_form.setObjectName(f"ReagentForm_{maxrow}") self.grid.addWidget(reg_form, maxrow,0,1,4) @@ -95,11 +95,11 @@ class KitAdder(QWidget): info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']} # logger.debug(f"kit info: {pformat(info)}") # logger.debug(f"kit reagents: {pformat(reagents)}") - info['reagent_types'] = reagents + info['reagent_roles'] = reagents # logger.debug(pformat(info)) # send to kit constructor kit = PydKit(name=info['kit_name']) - for reagent in info['reagent_types']: + for reagent in info['reagent_roles']: uses = { info['used_for']: {'sheet':reagent['sheet'], @@ -107,7 +107,7 @@ class KitAdder(QWidget): 'lot':reagent['lot'], 'expiry':reagent['expiry'] }} - kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses)) + kit.reagent_roles.append(PydReagentRole(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses)) # logger.debug(f"Output pyd object: {kit.__dict__}") sqlobj, result = kit.toSQL(self.ctx) report.add_result(result=result) @@ -125,11 +125,11 @@ class KitAdder(QWidget): # logger.debug(f"Hello from {self.__class__} parser!") info = {} reagents = [] - widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)] + widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentRoleForm)] for widget in widgets: # logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}") match widget: - case ReagentTypeForm(): + case ReagentRoleForm(): reagents.append(widget.parse_form()) case QLineEdit(): info[widget.objectName()] = widget.text() @@ -139,7 +139,7 @@ class KitAdder(QWidget): info[widget.objectName()] = widget.date().toPyDate() return info, reagents -class ReagentTypeForm(QWidget): +class ReagentRoleForm(QWidget): """ custom widget to add information about a new reagenttype """ @@ -152,13 +152,13 @@ class ReagentTypeForm(QWidget): self.reagent_getter = QComboBox() self.reagent_getter.setObjectName("rtname") # lookup all reagent type names from db - lookup = ReagentType.query() + lookup = ReagentRole.query() # logger.debug(f"Looked up ReagentType names: {lookup}") self.reagent_getter.addItems([item.name for item in lookup]) self.reagent_getter.setEditable(True) grid.addWidget(self.reagent_getter,0,1) grid.addWidget(QLabel("Extension of Life (months):"),0,2) - # widget to get extension of life + # NOTE: widget to get extension of life self.eol = QSpinBox() self.eol.setObjectName('eol') self.eol.setMinimum(0) diff --git a/src/submissions/frontend/widgets/misc.py b/src/submissions/frontend/widgets/misc.py index 3d6ae79..e0e84b2 100644 --- a/src/submissions/frontend/widgets/misc.py +++ b/src/submissions/frontend/widgets/misc.py @@ -23,10 +23,10 @@ class AddReagentForm(QDialog): """ dialog to add gather info about new reagent """ - def __init__(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, reagent_name:str|None=None) -> None: + def __init__(self, reagent_lot:str|None=None, reagent_role: str | None=None, expiry: date | None=None, reagent_name: str | None=None) -> None: super().__init__() if reagent_lot == None: - reagent_lot = reagent_type + reagent_lot = reagent_role self.setWindowTitle("Add Reagent") @@ -57,15 +57,15 @@ class AddReagentForm(QDialog): # widget to get reagent type info self.type_input = QComboBox() self.type_input.setObjectName('type') - self.type_input.addItems([item.name for item in ReagentType.query()]) + self.type_input.addItems([item.name for item in ReagentRole.query()]) # logger.debug(f"Trying to find index of {reagent_type}") # convert input to user friendly string? try: - reagent_type = reagent_type.replace("_", " ").title() + reagent_role = reagent_role.replace("_", " ").title() except AttributeError: - reagent_type = None + reagent_role = None # set parsed reagent type to top of list - index = self.type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith) + index = self.type_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith) if index >= 0: self.type_input.setCurrentIndex(index) self.layout = QVBoxLayout() @@ -91,7 +91,7 @@ class AddReagentForm(QDialog): return dict(name=self.name_input.currentText().strip(), lot=self.lot_input.text().strip(), expiry=self.exp_input.date().toPyDate(), - type=self.type_input.currentText().strip()) + role=self.type_input.currentText().strip()) def update_names(self): """ @@ -99,7 +99,7 @@ class AddReagentForm(QDialog): """ # logger.debug(self.type_input.currentText()) self.name_input.clear() - lookup = Reagent.query(reagent_type=self.type_input.currentText()) + lookup = Reagent.query(reagent_role=self.type_input.currentText()) self.name_input.addItems(list(set([item.name for item in lookup]))) class ReportDatePicker(QDialog): diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index a5ba527..07b2966 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -14,7 +14,7 @@ from backend.excel.parser import SheetParser from backend.validators import PydSubmission, PydReagent from backend.db import ( KitType, Organization, SubmissionType, Reagent, - ReagentType, KitTypeReagentTypeAssociation + ReagentRole, KitTypeReagentRoleAssociation ) from pprint import pformat from .pop_ups import QuestionAsker, AlertPop @@ -112,14 +112,14 @@ class SubmissionFormContainer(QWidget): # logger.debug(f"Outgoing report: {self.report.results}") # logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}") - def add_reagent(self, reagent_lot: str | None = None, reagent_type: str | None = None, expiry: date | None = None, + def add_reagent(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None, name: str | None = None): """ Action to create new reagent in DB. Args: reagent_lot (str | None, optional): Parsed reagent from import form. Defaults to None. - reagent_type (str | None, optional): Parsed reagent type from import form. Defaults to None. + reagent_role (str | None, optional): Parsed reagent type from import form. Defaults to None. expiry (date | None, optional): Parsed reagent expiry data. Defaults to None. name (str | None, optional): Parsed reagent name. Defaults to None. @@ -130,7 +130,7 @@ class SubmissionFormContainer(QWidget): if isinstance(reagent_lot, bool): reagent_lot = "" # NOTE: create form - dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name) + dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_role=reagent_role, expiry=expiry, reagent_name=name) if dlg.exec(): # extract form info info = dlg.parse_form() @@ -581,13 +581,13 @@ class SubmissionFormWidget(QWidget): """ lot = self.lot.currentText() # logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}") - wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type) + wanted_reagent = Reagent.query(lot_number=lot, reagent_role=self.reagent.role) # NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent) if wanted_reagent == None: dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?") if dlg.exec(): - wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, + wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_role=self.reagent.role, expiry=self.reagent.expiry, name=self.reagent.name) return wanted_reagent, None @@ -598,10 +598,10 @@ class SubmissionFormWidget(QWidget): else: # NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name # from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly. - rt = ReagentType.query(name=self.reagent.type) + rt = ReagentRole.query(name=self.reagent.role) if rt == None: - rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent) - return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, + rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent) + return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name, expiry=wanted_reagent.expiry, missing=False), None def updated(self): @@ -619,20 +619,20 @@ class SubmissionFormWidget(QWidget): check = not reagent.missing except: check = False - self.setObjectName(f"{reagent.type}_label") + self.setObjectName(f"{reagent.role}_label") if check: - self.setText(f"Parsed {reagent.type}") + self.setText(f"Parsed {reagent.role}") else: - self.setText(f"MISSING {reagent.type}") + self.setText(f"MISSING {reagent.role}") - def updated(self, reagent_type: str): + def updated(self, reagent_role: str): """ Marks widget as updated Args: - reagent_type (str): _description_ + reagent_role (str): _description_ """ - self.setText(f"UPDATED {reagent_type}") + self.setText(f"UPDATED {reagent_role}") class ReagentLot(QComboBox): @@ -641,7 +641,7 @@ class SubmissionFormWidget(QWidget): self.setEditable(True) # logger.debug(f"Attempting lookup of reagents by type: {reagent.type}") # NOTE: below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work. - lookup = Reagent.query(reagent_type=reagent.type) + lookup = Reagent.query(reagent_role=reagent.role) relevant_reagents = [str(item.lot) for item in lookup] output_reg = [] for rel_reagent in relevant_reagents: @@ -658,7 +658,7 @@ class SubmissionFormWidget(QWidget): if check_not_nan(reagent.lot): relevant_reagents.insert(0, str(reagent.lot)) else: - looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, + looked_up_rt = KitTypeReagentRoleAssociation.query(reagent_role=reagent.role, kit_type=extraction_kit) try: looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used) @@ -684,5 +684,5 @@ class SubmissionFormWidget(QWidget): # logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.") pass # logger.debug(f"New relevant reagents: {relevant_reagents}") - self.setObjectName(f"lot_{reagent.type}") + self.setObjectName(f"lot_{reagent.role}") self.addItems(relevant_reagents) diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicsubmission_details.html index 1633875..cc87dfb 100644 --- a/src/submissions/templates/basicsubmission_details.html +++ b/src/submissions/templates/basicsubmission_details.html @@ -47,7 +47,7 @@ {% endfor %}

Reagents:

{% for item in sub['reagents'] %} -     {{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
+     {{ item['role'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
{% endfor %}

{% if sub['equipment'] %}

Equipment: