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 Add the object to the database and commit
""" """
report = Report() 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: try:
self.__database_session__.add(self) self.__database_session__.add(self)
self.__database_session__.commit() self.__database_session__.commit()
@@ -566,7 +578,7 @@ class BaseClass(Base):
else: else:
return super().__setattr__(key, value) return super().__setattr__(key, value)
def delete(self): def delete(self, **kwargs):
logger.error(f"Delete has not been implemented for {self.__class__.__name__}") logger.error(f"Delete has not been implemented for {self.__class__.__name__}")
def rectify_query_date(input_date: datetime, eod: bool = False) -> str: 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 relevant = {k: v for k, v in self.__class__.__dict__.items() if
isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)} isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)}
output = {} output = {}
output['excluded'] = ["excluded", "misc_info", "_misc_info", "id"]
for k, v in relevant.items(): for k, v in relevant.items():
try: try:
check = v.foreign_keys check = v.foreign_keys
@@ -611,14 +624,55 @@ class BaseClass(Base):
value = getattr(self, k) value = getattr(self, k)
except AttributeError: except AttributeError:
continue 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: match value:
case datetime(): case datetime() | date():
value = value.strftime("%Y-%m-%d %H:%M:%S") 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 _: case _:
pass pass
output[k.strip("_")] = value output[k] = value
return output return output
def to_pydantic(self, pyd_model_name:str|None=None, **kwargs): def to_pydantic(self, pyd_model_name:str|None=None, **kwargs):
from backend.validators import pydant from backend.validators import pydant
if not pyd_model_name: if not pyd_model_name:

View File

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

View File

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

View File

@@ -1500,7 +1500,7 @@ class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True):
class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
proceduretype: ProcedureType | None = Field(default=None) proceduretype: ProcedureType | None = Field(default=None)
run: Run | None = Field(default=None) run: Run | str | None = Field(default=None)
name: dict = Field(default=dict(value="NA", missing=True), validate_default=True) name: dict = Field(default=dict(value="NA", missing=True), validate_default=True)
technician: dict = Field(default=dict(value="NA", missing=True)) technician: dict = Field(default=dict(value="NA", missing=True))
repeat: bool = Field(default=False) repeat: bool = Field(default=False)
@@ -1590,6 +1590,13 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
value = {item.name: item.reagent for item in kittype.reagentrole} value = {item.name: item.reagent for item in kittype.reagentrole}
return value return value
@field_validator("run")
@classmethod
def lookup_run(cls, value):
if isinstance(value, str):
value = Run.query(name=value)
return value
def update_kittype_reagentroles(self, kittype: str | KitType): def update_kittype_reagentroles(self, kittype: str | KitType):
if kittype == self.__class__.model_fields['kittype'].default['value']: if kittype == self.__class__.model_fields['kittype'].default['value']:
return return
@@ -1701,13 +1708,24 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
# NOTE: reset reagent associations. # NOTE: reset reagent associations.
sql.procedurereagentassociation = [] sql.procedurereagentassociation = []
for reagent in self.reagent: for reagent in self.reagent:
logger.debug(reagent)
if isinstance(reagent, dict): if isinstance(reagent, dict):
reagent = PydReagent(**reagent) reagent = PydReagent(**reagent)
logger.debug(reagent)
reagentrole = reagent.reagentrole
reagent = reagent.to_sql() reagent = reagent.to_sql()
logger.debug(reagentrole)
if reagent not in sql.reagent: if reagent not in sql.reagent:
# NOTE: Remove any previous association for this role.
removable = ProcedureReagentAssociation.query(procedure=sql, reagentrole=reagentrole)
logger.debug(f"Removable: {removable}")
if removable:
if isinstance(removable, list):
for r in removable:
r.delete()
else:
removable.delete()
logger.debug(f"Adding {reagent} to {sql}") logger.debug(f"Adding {reagent} to {sql}")
reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql) reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql, reagentrole=reagentrole)
try: try:
start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1
except ValueError: except ValueError:
@@ -1733,7 +1751,11 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
kittype = KitType.query(name=self.kittype['value'], limit=1) kittype = KitType.query(name=self.kittype['value'], limit=1)
if kittype: if kittype:
sql.kittype = kittype sql.kittype = kittype
logger.debug(self.reagent) # logger.debug(self.reagent)
# for reagent in self.reagent:
# reagent = reagent.to_sql()
# if reagent not in sql.reagent:
# reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql)
for equipment in self.equipment: for equipment in self.equipment:
equip = Equipment.query(name=equipment.name) equip = Equipment.query(name=equipment.name)
if equip not in sql.equipment: if equip not in sql.equipment:
@@ -1742,6 +1764,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
process = equipment.process.to_sql() process = equipment.process.to_sql()
equip_assoc.process = process equip_assoc.process = process
# logger.debug(f"Output sql: {[pformat(item.__dict__) for item in sql.procedureequipmentassociation]}") # logger.debug(f"Output sql: {[pformat(item.__dict__) for item in sql.procedureequipmentassociation]}")
logger.debug(pformat(sql.__dict__))
return sql, None return sql, None
@@ -1854,20 +1877,29 @@ class PydClientSubmission(PydBaseClass):
""" """
from frontend.widgets.submission_widget import ClientSubmissionFormWidget from frontend.widgets.submission_widget import ClientSubmissionFormWidget
if not samples: if not samples:
samples = self.samples samples = self.sample
return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable) return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable)
def to_sql(self): def to_sql(self):
sql = super().to_sql() sql = super().to_sql()
assert not any([isinstance(item, PydSample) for item in sql.sample])
sql.sample = []
if "info_placement" not in sql._misc_info: if "info_placement" not in sql._misc_info:
sql._misc_info['info_placement'] = [] sql._misc_info['info_placement'] = []
info_placement = [] info_placement = []
for k in list(self.model_fields.keys()) + list(self.model_extra.keys()): for k in list(self.model_fields.keys()) + list(self.model_extra.keys()):
logger.debug(f"Running {k}")
attribute = getattr(self, k) attribute = getattr(self, k)
match k: match k:
case "filepath": case "filepath":
sql._misc_info[k] = attribute.__str__() sql._misc_info[k] = attribute.__str__()
continue continue
# case "sample":
# sample = [item.to_sql() for item in self.sample]
# logger.debug(sample)
# for s in sample:
# logger.debug(f"adding {s}")
# sql.add_sample(sample=s)
case _: case _:
pass pass
logger.debug(f"Setting {k} to {attribute}") logger.debug(f"Setting {k} to {attribute}")
@@ -1876,7 +1908,7 @@ class PydClientSubmission(PydBaseClass):
info_placement.append(dict(name=k, location=attribute['location'])) info_placement.append(dict(name=k, location=attribute['location']))
else: else:
info_placement.append(dict(name=k, location=None)) info_placement.append(dict(name=k, location=None))
max_row = max([value['location']['row'] for value in info_placement if value]) # max_row = max([value['location']['row'] for value in info_placement if value])
sql._misc_info['info_placement'] = info_placement sql._misc_info['info_placement'] = info_placement
return sql return sql

View File

@@ -80,6 +80,7 @@ class ProcedureCreation(QDialog):
equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop( equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
equipmentrole['equipment'].index(item_in_er_list))) equipmentrole['equipment'].index(item_in_er_list)))
proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True) proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True)
self.update_equipment = EquipmentUsage.update_equipment
html = render_details_template( html = render_details_template(
template_name="procedure_creation", template_name="procedure_creation",
# css_in=['new_context_menu'], # css_in=['new_context_menu'],
@@ -94,6 +95,30 @@ class ProcedureCreation(QDialog):
f.write(html) f.write(html)
self.webview.setHtml(html) self.webview.setHtml(html)
@pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
from backend.db.models import Equipment
logger.debug("Updating equipment")
try:
equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
except StopIteration:
equipment_of_interest = None
equipment = Equipment.query(name=equipment)
if equipment_of_interest:
eoi = self.procedure.equipment.pop(self.procedure.equipment.index(equipment_of_interest))
else:
eoi = equipment.to_pydantic(proceduretype=self.procedure.proceduretype)
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)
logger.debug(f"Updated equipment: {self.procedure.equipment}")
@pyqtSlot(str, str) @pyqtSlot(str, str)
def text_changed(self, key: str, new_value: str): def text_changed(self, key: str, new_value: str):
logger.debug(f"New value for {key}: {new_value}") logger.debug(f"New value for {key}: {new_value}")

View File

@@ -63,17 +63,19 @@ class SubmissionDetails(QDialog):
self.webview.page().setWebChannel(self.channel) self.webview.page().setWebChannel(self.channel)
def object_details(self, object): def object_details(self, object):
details = object.details_dict() details = object.clean_details_dict(object.details_dict())
template = object.details_template template = object.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
key = object.__class__.__name__.lower() key = object.__class__.__name__.lower()
d = {key: details} d = {key: details}
logger.debug(f"Using details: {d}") logger.debug(f"Using details: {pformat(d)}")
html = template.render(**d, css=css) html = template.render(**d, css=[css])
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}") self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}")
with open(f"{object.__class__.__name__}_details_rendered.html", "w") as f:
f.write(html)
@@ -226,7 +228,7 @@ class SubmissionDetails(QDialog):
Args: Args:
run (str | BasicRun): Submission of interest. run (str | BasicRun): Submission of interest.
""" """
logger.debug(f"Submission details.") logger.debug(f"Run details.")
if isinstance(run, str): if isinstance(run, str):
run = Run.query(name=run) run = Run.query(name=run)
self.rsl_plate_number = run.rsl_plate_number self.rsl_plate_number = run.rsl_plate_number
@@ -242,6 +244,7 @@ class SubmissionDetails(QDialog):
self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css) self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html) self.webview.setHtml(self.html)
@pyqtSlot(str) @pyqtSlot(str)
def sign_off(self, run: str | Run) -> None: def sign_off(self, run: str | Run) -> None:
""" """

View File

@@ -1,6 +1,8 @@
""" """
Contains all procedure related frontend functions Contains all procedure related frontend functions
""" """
import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout
@@ -142,9 +144,9 @@ class SubmissionFormContainer(QWidget):
# self.pydclientsubmission = self.clientsubmissionparser.to_pydantic() # self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
# self.pydsamples = self.sampleparser.to_pydantic() # self.pydsamples = self.sampleparser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}") # logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, fname=fname) self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname)
self.pydclientsubmission = self.clientsubmission_manager.parse() self.pydclientsubmission = self.clientsubmission_manager.parse()
checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.samples) checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.sample)
if checker.exec(): if checker.exec():
# logger.debug(pformat(self.pydclientsubmission.sample)) # logger.debug(pformat(self.pydclientsubmission.sample))
try: try:
@@ -189,7 +191,7 @@ class SubmissionFormContainer(QWidget):
class SubmissionFormWidget(QWidget): class SubmissionFormWidget(QWidget):
update_reagent_fields = ['kittype'] update_reagent_fields = ['kittype']
def __init__(self, parent: QWidget, pyd: PydRun, disable: list | None = None) -> None: def __init__(self, parent: QWidget, pyd: PydClientSubmission, disable: list | None = None) -> None:
super().__init__(parent) super().__init__(parent)
if disable is None: if disable is None:
disable = [] disable = []
@@ -816,8 +818,8 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
except AttributeError: except AttributeError:
pass pass
# save_btn = QPushButton("Save") # save_btn = QPushButton("Save")
self.samples = samples self.sample = samples
logger.debug(f"Samples: {self.samples}") logger.debug(f"Samples: {self.sample}")
start_run_btn = QPushButton("Save") start_run_btn = QPushButton("Save")
# self.layout.addWidget(save_btn) # self.layout.addWidget(save_btn)
self.layout.addWidget(start_run_btn) self.layout.addWidget(start_run_btn)
@@ -867,12 +869,19 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
@report_result @report_result
def create_new_submission(self, *args) -> Report: def create_new_submission(self, *args) -> Report:
pyd = self.to_pydantic() pyd = self.to_pydantic()
logger.debug(f"Pydantic: {pyd}")
sql = pyd.to_sql() sql = pyd.to_sql()
for sample in self.samples: for sample in pyd.sample:
if isinstance(sample, PydSample): if isinstance(sample, PydSample):
sample = sample.to_sql() sample = sample.to_sql()
assert not isinstance(sample, PydSample)
# if sample not in sql.sample:
sql.add_sample(sample=sample) sql.add_sample(sample=sample)
logger.debug(sql.__dict__) logger.debug(pformat(sql.__dict__))
try:
del sql._misc_info['sample']
except KeyError:
pass
sql.save() sql.save()
self.app.table_widget.sub_wid.set_data() self.app.table_widget.sub_wid.set_data()
self.setParent(None) self.setParent(None)

View File

@@ -16,22 +16,22 @@
</p> </p>
{% if clientsubmission['sample'] %} {% if clientsubmission['sample'] %}
<button type="button" class="collapsible"><h3><u>Client Submitted Samples:</u></h3></button> <button type="button"><h3><u>Client Submitted Samples:</u></h3></button>
<div class="nested"> <!-- <div class="nested">-->
<p>{% for sample in clientsubmission['sample'] %} <p>{% for sample in clientsubmission['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="data-link sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id'] }}</a><br> &nbsp;&nbsp;&nbsp;&nbsp;<a class="data-link sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id'] }}</a><br>
{% endfor %}</p> {% endfor %}</p>
</div> <!-- </div>-->
{% endif %} {% endif %}
{% if clientsubmission['run'] %} {% if clientsubmission['run'] %}
<button type="button" class="collapsible"><h3><u>Runs:</u></h3></button> <button type="button"><h3><u>Runs:</u></h3></button>
<div class="nested"> <!-- <div class="nested">-->
{% for run in clientsubmission['run'] %} {% for run in clientsubmission['run'] %}
{% with run=run, child=True %} {% with run=run, child=True %}
{% include "run_details.html" %} {% include "run_details.html" %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</div> <!-- </div>-->
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>

View File

@@ -24,23 +24,7 @@
{% block script %} {% block script %}
{% if not child %} {% if not child %}
<!--<script>-->
<!--var coll = document.getElementsByClassName("collapsible");-->
<!--var i;-->
<!--for (i = 0; i < coll.length; i++) {-->
<!-- coll[i].addEventListener("click", function() {-->
<!-- this.classList.toggle("active");-->
<!-- var content = this.nextElementSibling;-->
<!-- if (content.style.display === "block") {-->
<!-- content.style.display = "none";-->
<!-- } else {-->
<!-- content.style.display = "block";-->
<!-- }-->
<!-- });-->
<!--}-->
<!--</script>-->
<!--{% endif %}-->
{% for j in js%} {% for j in js%}
<script> <script>
@@ -48,6 +32,7 @@
</script> </script>
{% endfor %} {% endfor %}
{% endif %}
{% endblock %} {% endblock %}
{% if not child %} {% if not child %}
</html> </html>

View File

@@ -15,23 +15,51 @@
<p>{% for key, value in procedure.items() if key not in procedure['excluded'] %} <p>{% for key, value in procedure.items() if key not in procedure['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: {{ value }}</b><br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: {{ value }}</b><br>
{% endfor %}</p> {% endfor %}</p>
{% if procedure['reagent'] %}
<button type="button"><h3><u>Reagents:</u></h3></button>
&nbsp;&nbsp;&nbsp;&nbsp;<table style="border: 1px solid black; width: 100%; text-align: center">
<tr>
<th style="border: 1px solid black;">Reagent Role</th>
<th style="border: 1px solid black;">Reagent Name</th>
<th style="border: 1px solid black;">Lot</th>
<th style="border: 1px solid black;">Expiry</th>
</tr>
{% for reg in procedure['reagent'] %}
{% with reagent=reg, child=True %}
{% include "support/reagent_list.html" %}
{% endwith %}
{% endfor %}</p>
</table><br/>
{% endif %}
{% if procedure['equipment'] %}
<button type="button"><h3><u>Equipment:</u></h3></button>
&nbsp;&nbsp;&nbsp;&nbsp;<table style="border: 1px solid black; width: 100%; text-align: center">
<tr>
<th style="border: 1px solid black;">Equipment Role</th>
<th style="border: 1px solid black;">Equipment Name</th>
<th style="border: 1px solid black;">Process</th>
<th style="border: 1px solid black;">Tips</th>
</tr>
{% for eq in procedure['equipment'] %}
{% with equipment=eq, child=True %}
{% include "support/equipment_list.html" %}
{% endwith %}
{% endfor %}
</table><br/>
{% endif %}
{% if procedure['results'] %} {% if procedure['results'] %}
<button type="button" class="collapsible"><h3><u>Results:</u></h3></button> <button type="button"><h3><u>Results:</u></h3></button>
<div class="nested">
{% for result in procedure['results'] %} {% for result in procedure['results'] %}
<p>{% for k, v in result['result'].items() %} <p>{% for k, v in result['result'].items() %}
<b>{{ key | replace("_", " ") | title | replace("Rsl", "RSL") }}:</b> {{ value }}<br> <b>{{ key | replace("_", " ") | title | replace("Rsl", "RSL") }}:</b> {{ value }}<br>
{% endfor %}</p> {% endfor %}</p>
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
{% if procedure['sample'] %} {% if procedure['sample'] %}
<button type="button" class="collapsible"><h3><u>Procedure Samples:</u></h3></button> <button type="button"><h3><u>Procedure Samples:</u></h3></button>
<div class="nested">
<p>{% for sample in procedure['sample'] %} <p>{% for sample in procedure['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br> &nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p> {% endfor %}</p>
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -15,20 +15,18 @@
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Rsl", "RSL") }}:</b> {{ value }}<br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Rsl", "RSL") }}:</b> {{ value }}<br>
{% endfor %}</p> {% endfor %}</p>
{% if run['sample'] %} {% if run['sample'] %}
<button type="button" class="collapsible"><h3><u>Run Samples:</u></h3></button> <button type="button"><h3><u>Run Samples:</u></h3></button>
<p>{% for sample in run['sample'] %} <p>{% for sample in run['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br> &nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}
{% if run['procedure'] %} {% if run['procedure'] %}
<button type="button" class="collapsible"><h3><u>Procedures:</u></h3></button> <button type="button"><h3><u>Procedures:</u></h3></button>
<div class="nested">
{% for procedure in run['procedure'] %} {% for procedure in run['procedure'] %}
{% with procedure=procedure, child=True %} {% with procedure=procedure, child=True %}
{% include "procedure_details.html" %} {% include "procedure_details.html" %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block signing_button %} {% block signing_button %}
@@ -55,5 +53,3 @@
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,6 @@
<tr>
<td style="border: 1px solid black;">{{ reagent['reagentrole'] }}</td>
<td style="border: 1px solid black;">{{ reagent['name'] }}</td>
<td style="border: 1px solid black;">{{ reagent['lot'] }}</td>
<td style="border: 1px solid black;">{{ reagent['expiry'].strftime('%Y-%m-%d') }}</td>
</tr>