Updates to details interface.

This commit is contained in:
lwark
2025-07-16 14:12:03 -05:00
parent 02c88256f2
commit 53c6668ce1
12 changed files with 276 additions and 71 deletions

View File

@@ -342,6 +342,18 @@ class BaseClass(Base):
Add the object to the database and commit
"""
report = Report()
del_keys = []
try:
items = self._misc_info.items()
except AttributeError:
items = []
for key, value in items:
try:
json.dumps(value)
except TypeError as e:
del_keys.append(key)
for dk in del_keys:
del self._misc_info[dk]
try:
self.__database_session__.add(self)
self.__database_session__.commit()
@@ -566,7 +578,7 @@ class BaseClass(Base):
else:
return super().__setattr__(key, value)
def delete(self):
def delete(self, **kwargs):
logger.error(f"Delete has not been implemented for {self.__class__.__name__}")
def rectify_query_date(input_date: datetime, eod: bool = False) -> str:
@@ -600,6 +612,7 @@ class BaseClass(Base):
relevant = {k: v for k, v in self.__class__.__dict__.items() if
isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)}
output = {}
output['excluded'] = ["excluded", "misc_info", "_misc_info", "id"]
for k, v in relevant.items():
try:
check = v.foreign_keys
@@ -611,14 +624,55 @@ class BaseClass(Base):
value = getattr(self, k)
except AttributeError:
continue
# match value:
# case datetime():
# value = value.strftime("%Y-%m-%d %H:%M:%S")
# case bytes():
# continue
# case dict():
# try:
# value = value['name']
# except KeyError:
# if k == "_misc_info":
# value = value
# else:
# continue
# case _:
# pass
output[k.strip("_")] = value
# output = self.clean_details_dict(output)
return output
@classmethod
def clean_details_dict(cls, dictionary):
output = {}
for k, value in dictionary.items():
match value:
case datetime():
value = value.strftime("%Y-%m-%d %H:%M:%S")
case datetime() | date():
value = value.strftime("%Y-%m-%d")
case bytes():
continue
case dict():
try:
value = value['name']
except KeyError:
if k == "_misc_info" or k == "misc_info":
value = value
else:
continue
case x if issubclass(value.__class__, cls):
try:
value = value.name
except AttributeError:
continue
case _:
pass
output[k.strip("_")] = value
output[k] = value
return output
def to_pydantic(self, pyd_model_name:str|None=None, **kwargs):
from backend.validators import pydant
if not pyd_model_name:

View File

@@ -17,6 +17,9 @@ from pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, ClientLab, LogMixin
from io import BytesIO
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError, \
ArgumentError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
if TYPE_CHECKING:
from backend.db.models.submissions import Run, ProcedureSampleAssociation
@@ -804,6 +807,8 @@ class Reagent(BaseClass, LogMixin):
output = super().details_dict()
if reagentrole:
output['reagentrole'] = reagentrole
else:
output['reagentrole'] = self.reagentrole[0].name
return output
@@ -1358,6 +1363,8 @@ class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True)
name = Column(String, unique=True)
repeat = Column(INTEGER, nullable=False)
started_date = Column(TIMESTAMP)
completed_date = Column(TIMESTAMP)
technician = Column(String(64)) #: name of processing tech(s)
results = relationship("Results", back_populates="procedure", uselist=True)
@@ -1510,7 +1517,8 @@ class Procedure(BaseClass):
def details_dict(self, **kwargs):
output = super().details_dict()
output['kittype'] = output['kittype'].details_dict()
output['proceduretype'] = output['proceduretype'].details_dict()
output['kit_type'] = self.kittype.name
output['proceduretype'] = output['proceduretype'].details_dict()['name']
output['results'] = [result.details_dict() for result in output['results']]
run_samples = [sample for sample in self.run.sample]
active_samples = [sample.details_dict() for sample in output['proceduresampleassociation']
@@ -1530,10 +1538,11 @@ class Procedure(BaseClass):
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['excluded'] = ['id', "results", "proceduresampleassociation", "sample", "procedurereagentassociation",
output['run'] = self.run.name
output['excluded'] += ['id', "results", "proceduresampleassociation", "sample", "procedurereagentassociation",
"procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment",
"tips",
"excluded"]
"tips", "control", "kittype"]
# output = self.clean_details_dict(output)
return output
def to_pydantic(self, **kwargs):
@@ -1546,7 +1555,7 @@ class Procedure(BaseClass):
for reagent in output.reagent:
match reagent:
case dict():
reagent['reagentrole'] = next((reagentrole.name for reagentrole in self.kittype.reagentrole if reagentrole in reagent['reagentrole']), None)
reagent['reagentrole'] = next((reagentrole.name for reagentrole in self.kittype.reagentrole if reagentrole == reagent['reagentrole']), None)
reagents.append(PydReagent(**reagent))
case PydReagent():
reagents.append(reagent)
@@ -1942,6 +1951,7 @@ class ProcedureReagentAssociation(BaseClass):
reagent_id = Column(INTEGER, ForeignKey("_reagent.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",
@@ -1960,12 +1970,13 @@ class ProcedureReagentAssociation(BaseClass):
logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!")
return f"<ProcedureReagentAssociation(Unknown Submission & {self.reagent.lot})>"
def __init__(self, reagent=None, procedure=None):
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
self.procedure = procedure
self.reagentrole = reagentrole
self.comments = ""
@classmethod
@@ -1973,6 +1984,7 @@ class ProcedureReagentAssociation(BaseClass):
def query(cls,
procedure: Procedure | str | int | None = None,
reagent: Reagent | str | None = None,
reagentrole: str | None = None,
limit: int = 0) -> ProcedureReagentAssociation | List[ProcedureReagentAssociation]:
"""
Lookup SubmissionReagentAssociations of interest.
@@ -2003,6 +2015,8 @@ class ProcedureReagentAssociation(BaseClass):
query = query.join(Procedure).filter(Procedure.id == procedure)
case _:
pass
if reagentrole:
query = query.filter(cls.reagentrole==reagentrole)
return cls.execute_query(query=query, limit=limit)
def to_sub_dict(self, kittype) -> dict:
@@ -2034,6 +2048,14 @@ class ProcedureReagentAssociation(BaseClass):
# output['results'] = [result.details_dict() for result in output['results']]
return output
def delete(self, **kwargs):
self.__database_session__.delete(self)
try:
self.__database_session__.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
self.__database_session__.rollback()
raise e
class Equipment(BaseClass, LogMixin):
"""
@@ -2547,6 +2569,7 @@ class ProcedureEquipmentAssociation(BaseClass):
misc = output['misc_info']
output.update(relevant)
output['misc_info'] = misc
output['equipment_role'] = self.equipmentrole
output['process'] = self.process.details_dict()
try:
output['tips'] = self.tips.details_dict()
@@ -2752,7 +2775,7 @@ class Process(BaseClass):
output = {}
for k, v in self.details_dict().items():
if isinstance(v, list):
output[k] = [item.name for item in v]
output[k] = [item.name if issubclass(item.__class__, BaseClass) else item for item in v]
elif issubclass(v.__class__, BaseClass):
output[k] = v.name
else:

View File

@@ -288,7 +288,7 @@ class ClientSubmission(BaseClass, LogMixin):
except AssertionError:
logger.warning(f"Converting {sample} to sql.")
sample = sample.to_sql()
logger.debug(sample.__dict__)
# logger.debug(sample.__dict__)
try:
row = sample._misc_info['row']
except (KeyError, AttributeError):
@@ -297,8 +297,10 @@ class ClientSubmission(BaseClass, LogMixin):
column = sample._misc_info['column']
except KeyError:
column = 0
logger.debug(f"Sample: {sample}")
# logger.debug(f"Sample: {sample}")
submission_rank = sample._misc_info['submission_rank']
if sample in self.sample:
return
assoc = ClientSubmissionSampleAssociation(
sample=sample,
submission=self,
@@ -362,9 +364,11 @@ class ClientSubmission(BaseClass, LogMixin):
output['name'] = self.name
output['client_lab'] = output['clientlab']
output['submission_type'] = output['submissiontype']
output['excluded'] = ['run', "sample", "clientsubmissionsampleassociation", "excluded",
output['excluded'] += ['run', "sample", "clientsubmissionsampleassociation", "excluded",
"expanded", 'clientlab', 'submissiontype', 'id']
output['expanded'] = ["clientlab", "contact", "submissiontype"]
# output = self.clean_details_dict(output)
logger.debug(f"{self.__class__.__name__}\n\n{pformat(output)}")
return output
def to_pydantic(self, filepath: Path | str | None = None, **kwargs):
@@ -383,14 +387,14 @@ class Run(BaseClass, LogMixin):
clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL",
name="fk_BS_clientsub_id")) #: client lab id from _organizations)
clientsubmission = relationship("ClientSubmission", back_populates="run")
started_date = Column(TIMESTAMP) #: Date this procedure was started.
_started_date = Column(TIMESTAMP) #: Date this procedure was started.
run_cost = Column(
FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kittype costs at time of creation.
signed_by = Column(String(32)) #: user name of person who submitted the procedure to the database.
comment = Column(JSON) #: user notes
custom = Column(JSON)
completed_date = Column(TIMESTAMP)
_completed_date = Column(TIMESTAMP)
procedure = relationship("Procedure", back_populates="run", uselist=True)
@@ -429,6 +433,38 @@ class Run(BaseClass, LogMixin):
def plate_number(self):
return self.rsl_plate_number
@hybrid_property
def started_date(self):
if self._started_date:
return self._started_date
else:
value = min([proc.started_date for proc in self.procedure])
return value
@started_date.setter
def started_date(self, value):
if value:
self._started_date = value
else:
self._started_date = min([proc.started_date for proc in self.procedure])
@hybrid_property
def completed_date(self):
if not self.signed_by:
return None
if self._completed_date:
return self._completed_date
else:
value = max([proc.completed_date for proc in self.procedure])
return value
@completed_date.setter
def completed_date(self, value):
if value:
self._completed_date = value
else:
self._completed_date = min([proc.started_date for proc in self.procedure])
@classmethod
def get_default_info(cls, *args, submissiontype: SubmissionType | None = None) -> dict:
"""
@@ -635,6 +671,8 @@ class Run(BaseClass, LogMixin):
def sample_count(self):
return len(self.sample)
def details_dict(self, **kwargs):
output = super().details_dict()
output['plate_number'] = self.plate_number
@@ -654,9 +692,12 @@ class Run(BaseClass, LogMixin):
output['sample'] = active_samples + inactive_samples
output['procedure'] = [procedure.details_dict() for procedure in output['procedure']]
output['permission'] = is_power_user()
output['excluded'] = ['procedure', "runsampleassociation", 'excluded', 'expanded', 'sample', 'id', 'custom',
'permission']
output['excluded'] += ['procedure', "runsampleassociation", 'excluded', 'expanded', 'sample', 'id', 'custom',
'permission', "clientsubmission"]
output['sample_count'] = self.sample_count
output['client_submission'] = self.clientsubmission.name
output['started_date'] = self.started_date
output['completed_date'] = self.completed_date
return output
@classmethod
@@ -1715,6 +1756,8 @@ class ClientSubmissionSampleAssociation(BaseClass):
output['misc_info'] = misc
# output['sample'] = temp
# output.update(output['sample'].details_dict())
# sys.exit()
return output
def to_pydantic(self) -> "PydSample":
@@ -2132,6 +2175,7 @@ class RunSampleAssociation(BaseClass):
# logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant)
output['misc_info'] = misc
return output