Renaming ReagentType to ReagentRole

This commit is contained in:
lwark
2024-06-03 13:32:04 -05:00
parent 1a716984a7
commit cdcce80898
10 changed files with 182 additions and 140 deletions

View File

@@ -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]

View File

@@ -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"<KitType({self.name})>"
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"<ReagentType({self.name})>"
return f"<ReagentRole({self.name})>"
@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"<Reagent({self.name}-{self.lot})>"
else:
return f"<Reagent({self.type.name}-{self.lot})>"
return f"<Reagent({self.role.name}-{self.lot})>"
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"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
return f"<KitTypeReagentRoleAssociation({self.kit_type} & {self.reagent_role})>"
@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):
# 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"<TipRole({self.name})>"
#
# 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"<Tips({self.name})>"

View File

@@ -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}")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)

View File

@@ -47,7 +47,7 @@
{% endfor %}</p>
<h3><u>Reagents:</u></h3>
<p>{% for item in sub['reagents'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['type'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
{% endfor %}</p>
{% if sub['equipment'] %}
<h3><u>Equipment:</u></h3>