Update of Equipment usage UI

This commit is contained in:
lwark
2025-06-27 13:32:36 -05:00
parent d8c3f3bbb2
commit 054345e3e1
11 changed files with 644 additions and 140 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import sys, logging, json import sys, logging, json
import sqlalchemy.exc
from dateutil.parser import parse from dateutil.parser import parse
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
@@ -239,14 +240,23 @@ class BaseClass(Base):
allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute)] allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute)]
# and not isinstance(v.property, _RelationshipDeclared)] # and not isinstance(v.property, _RelationshipDeclared)]
sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed} sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed}
logger.debug(f"Sanitized kwargs: {sanitized_kwargs}") # logger.debug(f"Sanitized kwargs: {sanitized_kwargs}")
instance = cls.query(**sanitized_kwargs) instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list): if not instance or isinstance(instance, list):
instance = cls() instance = cls()
new = True new = True
for k, v in sanitized_kwargs.items(): for k, v in sanitized_kwargs.items():
logger.debug(f"QorC Setting {k} to {v}") # logger.debug(f"QorC Setting {k} to {v}")
setattr(instance, k, v) if k == "id":
continue
try:
setattr(instance, k, v)
except AttributeError as e:
from backend.validators.pydant import PydBaseClass
if issubclass(v.__class__, PydBaseClass):
setattr(instance, k, v.to_sql())
else:
logger.error(f"Could not set {k} due to {e}")
logger.info(f"Instance from query or create: {instance}, new: {new}") logger.info(f"Instance from query or create: {instance}, new: {new}")
return instance, new return instance, new
@@ -315,9 +325,13 @@ class BaseClass(Base):
try: try:
self.__database_session__.add(self) self.__database_session__.add(self)
self.__database_session__.commit() self.__database_session__.commit()
# except sqlalchemy.exc.IntegrityError as i:
# logger.error(f"Integrity error saving {self} due to: {i}")
# logger.error(pformat(self.__dict__))
except Exception as e: except Exception as e:
logger.critical(f"Problem saving object: {e}") logger.critical(f"Problem saving {self} due to: {e}")
logger.error(f"Error message: {type(e)}") logger.error(f"Error message: {type(e)}")
logger.error(pformat(self.__dict__))
self.__database_session__.rollback() self.__database_session__.rollback()
report.add_result(Result(msg=e, status="Critical")) report.add_result(Result(msg=e, status="Critical"))
return report return report
@@ -484,9 +498,9 @@ class BaseClass(Base):
# logger.debug(f"Setting ColumnProperty to {value}") # logger.debug(f"Setting ColumnProperty to {value}")
return super().__setattr__(key, value) return super().__setattr__(key, value)
case _RelationshipDeclared(): case _RelationshipDeclared():
logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}") # logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}")
if field_type.property.uselist: if field_type.property.uselist:
logger.debug(f"Setting with uselist") # logger.debug(f"Setting with uselist")
existing = self.__getattribute__(key) existing = self.__getattribute__(key)
# NOTE: This is causing problems with removal of items from lists. Have to overhaul it. # NOTE: This is causing problems with removal of items from lists. Have to overhaul it.
if existing is not None: if existing is not None:
@@ -502,8 +516,11 @@ class BaseClass(Base):
pass pass
else: else:
value = [value] value = [value]
value = list(set(value)) try:
logger.debug(f"Final value for {key}: {value}") value = list(set(value))
except TypeError:
pass
# logger.debug(f"Final value for {key}: {value}")
return super().__setattr__(key, value) return super().__setattr__(key, value)
else: else:
if isinstance(value, list): if isinstance(value, list):
@@ -514,7 +531,7 @@ class BaseClass(Base):
try: try:
return super().__setattr__(key, value) return super().__setattr__(key, value)
except AttributeError: except AttributeError:
logger.debug(f"Possible attempt to set relationship {key} to simple var type. {value}") logger.warning(f"Possible attempt to set relationship {key} to simple var type. {value}")
relationship_class = field_type.property.entity.entity relationship_class = field_type.property.entity.entity
value = relationship_class.query(name=value) value = relationship_class.query(name=value)
try: try:
@@ -555,7 +572,7 @@ class BaseClass(Base):
output_date = datetime.combine(output_date, addition_time).strftime("%Y-%m-%d %H:%M:%S") output_date = datetime.combine(output_date, addition_time).strftime("%Y-%m-%d %H:%M:%S")
return output_date return output_date
def details_dict(self): def details_dict(self, **kwargs):
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 = {}
@@ -578,6 +595,16 @@ class BaseClass(Base):
output[k.strip("_")] = value output[k.strip("_")] = value
return output return output
def to_pydantic(self, **kwargs):
from backend.validators import pydant
pyd_model_name = f"Pyd{self.__class__.__name__}"
logger.debug(f"Looking for pydant model {pyd_model_name}")
try:
pyd = getattr(pydant, pyd_model_name)
except AttributeError:
raise AttributeError(f"Could not get pydantic class {pyd_model_name}")
return pyd(**self.details_dict())
def show_details(self, obj): def show_details(self, obj):
logger.debug("Show Details") logger.debug("Show Details")
from frontend.widgets.submission_details import SubmissionDetails from frontend.widgets.submission_details import SubmissionDetails

View File

@@ -4,14 +4,16 @@ All kittype and reagent related models
from __future__ import annotations from __future__ import annotations
import zipfile, logging, re import zipfile, logging, re
from operator import itemgetter from operator import itemgetter
from pprint import pformat
import numpy as np import numpy as np
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \ from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
jinja_template_loading jinja_template_loading, flatten_list
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path from pathlib import Path
@@ -20,6 +22,7 @@ from io import BytesIO
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.db.models.submissions import Run, ProcedureSampleAssociation from backend.db.models.submissions import Run, ProcedureSampleAssociation
from backend.validators.pydant import PydSample, PydResults
logger = logging.getLogger(f'procedure.{__name__}') logger = logging.getLogger(f'procedure.{__name__}')
@@ -127,6 +130,11 @@ class KitType(BaseClass):
process = relationship("Process", back_populates="kittype", process = relationship("Process", back_populates="kittype",
secondary=kittype_process) #: equipment process used by this kittype secondary=kittype_process) #: equipment process used by this kittype
proceduretypeequipmentroleassociation = relationship("ProcedureTypeEquipmentRoleAssociation", back_populates="kittype",
cascade="all, delete-orphan",)
equipmentrole = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole")
kittypereagentroleassociation = relationship( kittypereagentroleassociation = relationship(
"KitTypeReagentRoleAssociation", "KitTypeReagentRoleAssociation",
back_populates="kittype", back_populates="kittype",
@@ -148,6 +156,8 @@ class KitType(BaseClass):
creator=lambda ST: ProcedureTypeKitTypeAssociation( creator=lambda ST: ProcedureTypeKitTypeAssociation(
submissiontype=ST)) #: Association proxy to SubmissionTypeKitTypeAssociation submissiontype=ST)) #: Association proxy to SubmissionTypeKitTypeAssociation
@classproperty @classproperty
def aliases(cls) -> List[str]: def aliases(cls) -> List[str]:
""" """
@@ -174,17 +184,30 @@ class KitType(BaseClass):
""" """
match proceduretype: match proceduretype:
case ProcedureType(): case ProcedureType():
relevant_associations = [item for item in self.kittypereagentroleassociation if relevant_associations = [assoc for assoc in self.kittypereagentroleassociation if
assoc.proceduretype == proceduretype]
case str():
relevant_associations = [assoc for assoc in self.kittypereagentroleassociation if
assoc.proceduretype.name == proceduretype]
case _:
relevant_associations = [assoc for assoc in self.kittypereagentroleassociation]
if required_only:
return (assoc.reagentrole for assoc in relevant_associations if assoc.required == 1)
else:
return (assoc.reagentrole for assoc in relevant_associations)
def get_equipmentroles(self, proceduretype: str| ProcedureType | None = None) -> Generator[ReagentRole, None, None]:
match proceduretype:
case ProcedureType():
relevant_associations = [item for item in self.proceduretypeequipmentroleassociation if
item.proceduretype == proceduretype] item.proceduretype == proceduretype]
case str(): case str():
relevant_associations = [item for item in self.kittypereagentroleassociation if relevant_associations = [item for item in self.proceduretypeequipmentroleassociation if
item.proceduretype.name == proceduretype] item.proceduretype.name == proceduretype]
case _: case _:
relevant_associations = [item for item in self.kittypereagentroleassociation] relevant_associations = [item for item in self.proceduretypeequipmentroleassociation]
if required_only: return (assoc.equipmentrole for assoc in relevant_associations)
return (item.reagentrole for item in relevant_associations if item.required == 1)
else:
return (item.reagentrole for item in relevant_associations)
def construct_xl_map_for_use(self, proceduretype: str | SubmissionType) -> Tuple[dict | None, KitType]: def construct_xl_map_for_use(self, proceduretype: str | SubmissionType) -> Tuple[dict | None, KitType]:
""" """
@@ -1223,26 +1246,24 @@ class ProcedureType(BaseClass):
output = super().details_dict(**kwargs) output = super().details_dict(**kwargs)
output['kittype'] = [item.details_dict() for item in output['kittype']] output['kittype'] = [item.details_dict() for item in output['kittype']]
# output['process'] = [item.details_dict() for item in output['process']] # output['process'] = [item.details_dict() for item in output['process']]
output['equipment'] = [item.details_dict() for item in output['equipment']] output['equipment'] = [item.details_dict(proceduretype=self) for item in output['equipment']]
return output return output
def construct_dummy_procedure(self, run: Run | None = None):
def construct_dummy_procedure(self, run: Run|None=None):
from backend.validators.pydant import PydProcedure from backend.validators.pydant import PydProcedure
if run: if run:
samples = run.constuct_sample_dicts_for_proceduretype(proceduretype=self) samples = run.constuct_sample_dicts_for_proceduretype(proceduretype=self)
else:
samples = []
output = dict( output = dict(
proceduretype=self, proceduretype=self,
#name=dict(value=self.name, missing=True),
#possible_kits=[kittype.name for kittype in self.kittype],
repeat=False, repeat=False,
# plate_map=plate_map run=run,
run=run sample=samples
) )
return PydProcedure(**output) return PydProcedure(**output)
def construct_plate_map(self, sample_dicts: List[dict]) -> str: def construct_plate_map(self, sample_dicts: List["PydSample"]) -> str:
""" """
Constructs an html based plate map for procedure details. Constructs an html based plate map for procedure details.
@@ -1256,23 +1277,31 @@ class ProcedureType(BaseClass):
""" """
if self.plate_rows == 0 or self.plate_columns == 0: if self.plate_rows == 0 or self.plate_columns == 0:
return "<br/>" return "<br/>"
# plate_rows = range(1, self.plate_rows + 1) sample_dicts = self.pad_sample_dicts(sample_dicts=sample_dicts)
# plate_columns = range(1, self.plate_columns + 1) logger.debug(f"Sample dicts: {pformat(sample_dicts)}")
# total_wells = self.plate_columns * self.plate_rows
vw = round((-0.07 * len(sample_dicts)) + 12.2, 1) vw = round((-0.07 * len(sample_dicts)) + 12.2, 1)
# sample_dicts = run.constuct_sample_dicts_for_proceduretype(proceduretype=self)
# output_samples = [next((item for item in sample_dicts if item['row'] == row and item['column'] == column),
# dict(sample_id="", row=row, column=column, background_color="#ffffff"))
# for row in plate_rows
# for column in plate_columns]
# logger.debug(f"Output samples:\n{pformat(output_samples)}")
# NOTE: An overly complicated list comprehension create a list of sample locations # NOTE: An overly complicated list comprehension create a list of sample locations
# NOTE: next will return a blank cell if no value found for row/column # NOTE: next will return a blank cell if no value found for row/column
env = jinja_template_loading() env = jinja_template_loading()
template = env.get_template("support/plate_map.html") template = env.get_template("support/plate_map.html")
html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=sample_dicts, vw=vw) html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=sample_dicts,
vw=vw)
return html + "<br/>" return html + "<br/>"
def pad_sample_dicts(self, sample_dicts: List["PydSample"]):
from backend.validators.pydant import PydSample
output = []
logger.debug(f"Rows: {self.plate_rows}")
logger.debug(f"Columns: {self.plate_columns}")
for row, column in self.ranked_plate.values():
sample = next((sample for sample in sample_dicts if sample.row == row and sample.column == column),
PydSample(**dict(sample_id="", row=row, column=column, enabled=False)))
sample.background_color = "#6ffe1d" if sample.enabled else "#ffffff"
output.append(sample)
logger.debug(f"Appending {sample} at row {row}, column {column}")
return output
@property @property
def ranked_plate(self): def ranked_plate(self):
matrix = np.array([[0 for yyy in range(1, self.plate_rows + 1)] for xxx in range(1, self.plate_columns + 1)]) matrix = np.array([[0 for yyy in range(1, self.plate_rows + 1)] for xxx in range(1, self.plate_columns + 1)])
@@ -1318,7 +1347,7 @@ class Procedure(BaseClass):
) #: Relation to ProcedureReagentAssociation ) #: Relation to ProcedureReagentAssociation
reagent = association_proxy("procedurereagentassociation", reagent = association_proxy("procedurereagentassociation",
"reagent", creator=lambda reg: ProcedureReagentAssociation( "reagent", creator=lambda reg: ProcedureReagentAssociation(
reagent=reg)) #: Association proxy to RunReagentAssociation.reagent reagent=reg)) #: Association proxy to RunReagentAssociation.reagent
procedureequipmentassociation = relationship( procedureequipmentassociation = relationship(
@@ -1365,7 +1394,7 @@ class Procedure(BaseClass):
pass pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_dict(self, full_data: bool=False): def to_dict(self, full_data: bool = False):
output = dict() output = dict()
output['name'] = self.name output['name'] = self.name
return output return output
@@ -1381,7 +1410,7 @@ class Procedure(BaseClass):
names = ["Add Results", "Add Equipment", "Edit", "Add Comment", "Show Details", "Delete"] names = ["Add Results", "Add Equipment", "Edit", "Add Comment", "Show Details", "Delete"]
return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names} return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names}
def add_results(self, obj, resultstype_name:str): def add_results(self, obj, resultstype_name: str):
logger.debug(f"Add Results! {resultstype_name}") logger.debug(f"Add Results! {resultstype_name}")
from ...managers import results from ...managers import results
results_class = getattr(results, resultstype_name) results_class = getattr(results, resultstype_name)
@@ -1395,30 +1424,35 @@ class Procedure(BaseClass):
obj (_type_): parent widget obj (_type_): parent widget
""" """
logger.debug(f"Add equipment") logger.debug(f"Add equipment")
from frontend.widgets.equipment_usage import EquipmentUsage from frontend.widgets.equipment_usage_2 import EquipmentUsage
dlg = EquipmentUsage(parent=obj, procedure=self) dlg = EquipmentUsage(parent=obj, procedure=self.to_pydantic())
if dlg.exec(): if dlg.exec():
equipment = dlg.parse_form() # equipment = dlg.parse_form()
for equip in equipment: # for equip in equipment:
logger.debug(f"Parsed equipment: {equip}") # logger.debug(f"Parsed equipment: {equip}")
_, assoc = equip.to_sql(procedure=self) # _, assoc = equip.to_sql(procedure=self)
logger.debug(f"Got equipment association: {assoc} for {equip}") # logger.debug(f"Got equipment association: {assoc} for {equip}")
try: # try:
assoc.save() # assoc.save()
except AttributeError as e: # except AttributeError as e:
logger.error(f"Couldn't save association with {equip} due to {e}") # logger.error(f"Couldn't save association with {equip} due to {e}")
if equip.tips: # if equip.tips:
for tips in equip.tips: # for tips in equip.tips:
# logger.debug(f"Attempting to add tips assoc: {tips} (pydantic)") # # logger.debug(f"Attempting to add tips assoc: {tips} (pydantic)")
tassoc, _ = tips.to_sql(procedure=self) # tassoc, _ = tips.to_sql(procedure=self)
# logger.debug(f"Attempting to add tips assoc: {tips.__dict__} (sql)") # # logger.debug(f"Attempting to add tips assoc: {tips.__dict__} (sql)")
if tassoc not in self.proceduretipsassociation: # if tassoc not in self.proceduretipsassociation:
tassoc.save() # tassoc.save()
else: # else:
logger.error(f"Tips already found in submission, skipping.") # logger.error(f"Tips already found in submission, skipping.")
dlg.save_procedure()
def edit(self, obj): def edit(self, obj):
from frontend.widgets.procedure_creation import ProcedureCreation
logger.debug("Edit!") logger.debug("Edit!")
dlg = ProcedureCreation(parent=obj, procedure=self.to_pydantic())
if dlg.exec():
logger.debug("Edited")
def add_comment(self, obj): def add_comment(self, obj):
logger.debug("Add Comment!") logger.debug("Add Comment!")
@@ -1439,7 +1473,8 @@ class Procedure(BaseClass):
if sample.sample.sample_id in [s.sample_id for s in run_samples]] if sample.sample.sample_id in [s.sample_id for s in run_samples]]
for sample in active_samples: for sample in active_samples:
sample['active'] = True sample['active'] = True
inactive_samples = [sample.details_dict() for sample in run_samples if sample.name not in [s['sample_id'] for s in active_samples]] inactive_samples = [sample.details_dict() for sample in run_samples if
sample.name not in [s['sample_id'] for s in active_samples]]
# logger.debug(f"Inactive samples:{pformat(inactive_samples)}") # logger.debug(f"Inactive samples:{pformat(inactive_samples)}")
for sample in inactive_samples: for sample in inactive_samples:
sample['active'] = False sample['active'] = False
@@ -1451,10 +1486,30 @@ class Procedure(BaseClass):
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['excluded'] = ['id', "results", "proceduresampleassociation", "sample", "procedurereagentassociation",
"procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment", "tips", "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment",
"tips",
"excluded"] "excluded"]
return output return output
def to_pydantic(self, **kwargs):
from backend.validators.pydant import PydResults
output = super().to_pydantic()
output.kittype = dict(value=output.kittype['name'], missing=False)
results = []
for result in output.results:
match result:
case dict():
results.append(PydResults(**result))
case PydResults():
results.append(result)
case _:
pass
output.results = results
# for sample in output.sample:
# sample.enabled = True
return output
class ProcedureTypeKitTypeAssociation(BaseClass): class ProcedureTypeKitTypeAssociation(BaseClass):
""" """
Abstract of relationship between kits and their procedure type. Abstract of relationship between kits and their procedure type.
@@ -2046,11 +2101,16 @@ class Equipment(BaseClass, LogMixin):
PydEquipment: pydantic equipment object PydEquipment: pydantic equipment object
""" """
from backend.validators.pydant import PydEquipment from backend.validators.pydant import PydEquipment
creation_dict = self.details_dict()
processes = self.get_processes(proceduretype=proceduretype, kittype=kittype, processes = self.get_processes(proceduretype=proceduretype, kittype=kittype,
equipmentrole=equipmentrole) equipmentrole=equipmentrole)
logger.debug(f"Processes: {processes}")
creation_dict['processes'] = processes
logger.debug(f"EquipmentRole: {equipmentrole}") logger.debug(f"EquipmentRole: {equipmentrole}")
return PydEquipment(processes=processes, equipmentrole=equipmentrole, creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole']
**self.to_dict(processes=False)) # return PydEquipment(process=processes, equipmentrole=equipmentrole,
# **self.to_dict(processes=False))
return PydEquipment(**creation_dict)
@classproperty @classproperty
def manufacturer_regex(cls) -> re.Pattern: def manufacturer_regex(cls) -> re.Pattern:
@@ -2267,11 +2327,43 @@ class EquipmentRole(BaseClass):
return OmniEquipmentRole(instance_object=self, name=self.name) return OmniEquipmentRole(instance_object=self, name=self.name)
def details_dict(self, **kwargs): def details_dict(self, **kwargs):
if "proceduretype" in kwargs:
proceduretype = kwargs['proceduretype']
else:
proceduretype = None
match proceduretype:
case ProcedureType():
pass
case str():
proceduretype = ProcedureType.query(name=proceduretype, limit=1)
case _:
proceduretype = None
output = super().details_dict(**kwargs) output = super().details_dict(**kwargs)
# Note con
output['equipment'] = [item.details_dict() for item in output['equipment']] output['equipment'] = [item.details_dict() for item in output['equipment']]
output['equipment_json'] = []
equip = []
for eq in output['equipment']:
dicto = dict(name=eq['name'], asset_number=eq['asset_number'])
dicto['processes'] = [dict(name=process.name, tiprole=process.tiprole) for process in eq['process'] if proceduretype in process.proceduretype]
for process in dicto['processes']:
try:
process['tips'] = flatten_list([[tips.name for tips in tr.tips]for tr in process['tiprole']])
except KeyError:
logger.debug(f"process: {pformat(process)}")
raise KeyError()
del process['tiprole']
equip.append(dicto)
output['equipment_json'].append(dict(name=self.name, equipment=equip))
output['process'] = [item.details_dict() for item in output['process']] output['process'] = [item.details_dict() for item in output['process']]
try:
output['tips'] = [item.details_dict() for item in output['tips']]
except KeyError:
# logger.error(pformat(output))
pass
return output return output
class ProcedureEquipmentAssociation(BaseClass): class ProcedureEquipmentAssociation(BaseClass):
""" """
Abstract association between BasicRun and Equipment Abstract association between BasicRun and Equipment
@@ -2297,7 +2389,8 @@ class ProcedureEquipmentAssociation(BaseClass):
except AttributeError: except AttributeError:
return "<ProcedureEquipmentAssociation(Unknown)>" return "<ProcedureEquipmentAssociation(Unknown)>"
def __init__(self, procedure=None, equipment=None, procedure_id:int|None=None, equipment_id:int|None=None, equipmentrole: str = "None"): def __init__(self, procedure=None, equipment=None, procedure_id: int | None = None, equipment_id: int | None = None,
equipmentrole: str = "None"):
if not procedure: if not procedure:
if procedure_id: if procedure_id:
procedure = Procedure.query(id=procedure_id) procedure = Procedure.query(id=procedure_id)
@@ -2310,8 +2403,16 @@ class ProcedureEquipmentAssociation(BaseClass):
else: else:
logger.error("Creation error") logger.error("Creation error")
self.equipment = equipment self.equipment = equipment
if isinstance(equipmentrole, list):
equipmentrole = equipmentrole[0]
if isinstance(equipmentrole, EquipmentRole):
equipmentrole = equipmentrole.name
self.equipmentrole = equipmentrole self.equipmentrole = equipmentrole
@property
def name(self):
return f"{self.procedure.name} & {self.equipment.name}"
@property @property
def process(self): def process(self):
return Process.query(id=self.process_id) return Process.query(id=self.process_id)
@@ -2379,6 +2480,7 @@ class ProcedureEquipmentAssociation(BaseClass):
output['process'] = self.process.details_dict() output['process'] = self.process.details_dict()
return output return output
class ProcedureTypeEquipmentRoleAssociation(BaseClass): class ProcedureTypeEquipmentRoleAssociation(BaseClass):
""" """
Abstract association between SubmissionType and EquipmentRole Abstract association between SubmissionType and EquipmentRole
@@ -2386,6 +2488,8 @@ class ProcedureTypeEquipmentRoleAssociation(BaseClass):
equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"), proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
primary_key=True) #: id of associated procedure primary_key=True) #: id of associated procedure
kittype_id = Column(INTEGER, ForeignKey("_kittype.id"),
primary_key=True)
uses = Column(JSON) #: locations of equipment on the procedure type excel sheet. uses = Column(JSON) #: locations of equipment on the procedure type excel sheet.
static = Column(INTEGER, static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list? default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
@@ -2396,6 +2500,8 @@ class ProcedureTypeEquipmentRoleAssociation(BaseClass):
equipmentrole = relationship(EquipmentRole, equipmentrole = relationship(EquipmentRole,
back_populates="equipmentroleproceduretypeassociation") #: associated equipment back_populates="equipmentroleproceduretypeassociation") #: associated equipment
kittype = relationship(KitType, back_populates="proceduretypeequipmentroleassociation")
@validates('static') @validates('static')
def validate_static(self, key, value): def validate_static(self, key, value):
""" """
@@ -2781,6 +2887,10 @@ class Tips(BaseClass, LogMixin):
# template = env.get_template("tips_details.html") # template = env.get_template("tips_details.html")
# return template # return template
def to_pydantic(self, **kwargs):
output = super().to_pydantic()
return output
class ProcedureTypeTipRoleAssociation(BaseClass): class ProcedureTypeTipRoleAssociation(BaseClass):
""" """
@@ -2828,7 +2938,7 @@ class ProcedureTipsAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, tips: int|Tips, tiprole: str, procedure: int | Procedure | None = None, limit: int = 0, **kwargs) \ def query(cls, tips: int | Tips, tiprole: str, procedure: int | Procedure | None = None, limit: int = 0, **kwargs) \
-> Any | List[Any]: -> Any | List[Any]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match tips: match tips:
@@ -2873,15 +2983,15 @@ class ProcedureTipsAssociation(BaseClass):
class Results(BaseClass): class Results(BaseClass):
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
result_type = Column(String(32)) result_type = Column(String(32))
result = Column(JSON) result = Column(JSON)
date_analyzed = Column(TIMESTAMP)
procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL', procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL',
name="fk_RES_procedure_id")) name="fk_RES_procedure_id"))
procedure = relationship("Procedure", back_populates="results") procedure = relationship("Procedure", back_populates="results")
assoc_id = Column(INTEGER, ForeignKey("_proceduresampleassociation.id", ondelete='SET NULL', assoc_id = Column(INTEGER, ForeignKey("_proceduresampleassociation.id", ondelete='SET NULL',
name="fk_RES_ASSOC_id")) name="fk_RES_ASSOC_id"))
sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results") sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results")
_img = Column(String(128)) _img = Column(String(128))
@@ -2900,5 +3010,3 @@ class Results(BaseClass):
@image.setter @image.setter
def image(self, value): def image(self, value):
self._img = value self._img = value

View File

@@ -398,6 +398,10 @@ class Run(BaseClass, LogMixin):
def name(self): def name(self):
return self.rsl_plate_number return self.rsl_plate_number
@hybrid_property
def plate_number(self):
return self.rsl_plate_number
@classmethod @classmethod
def get_default_info(cls, *args, submissiontype: SubmissionType | None = None) -> dict: def get_default_info(cls, *args, submissiontype: SubmissionType | None = None) -> dict:
""" """
@@ -602,6 +606,7 @@ class Run(BaseClass, LogMixin):
def details_dict(self, **kwargs): def details_dict(self, **kwargs):
output = super().details_dict() output = super().details_dict()
output['plate_number'] = self.plate_number
submission_samples = [sample for sample in self.clientsubmission.sample] submission_samples = [sample for sample in self.clientsubmission.sample]
# logger.debug(f"Submission samples:{pformat(submission_samples)}") # logger.debug(f"Submission samples:{pformat(submission_samples)}")
active_samples = [sample.details_dict() for sample in output['runsampleassociation'] active_samples = [sample.details_dict() for sample in output['runsampleassociation']
@@ -1174,7 +1179,7 @@ class Run(BaseClass, LogMixin):
from frontend.widgets.procedure_creation import ProcedureCreation from frontend.widgets.procedure_creation import ProcedureCreation
procedure_type = next((proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name)) procedure_type = next((proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name))
logger.debug(f"Got ProcedureType: {procedure_type}") logger.debug(f"Got ProcedureType: {procedure_type}")
dlg = ProcedureCreation(parent=obj, run=self, proceduretype=procedure_type) dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self))
if dlg.exec(): if dlg.exec():
sql, _ = dlg.return_sql() sql, _ = dlg.return_sql()
logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}") logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}")
@@ -1358,12 +1363,12 @@ class Run(BaseClass, LogMixin):
row, column = plate_dict[submission_rank] row, column = plate_dict[submission_rank]
ranked_samples.append( ranked_samples.append(
dict(well_id=sample.sample_id, sample_id=sample.sample_id, row=row, column=column, submission_rank=submission_rank, dict(well_id=sample.sample_id, sample_id=sample.sample_id, row=row, column=column, submission_rank=submission_rank,
background_color="#6ffe1d")) background_color="#6ffe1d", enabled=True))
padded_list = [] padded_list = []
for iii in range(1, proceduretype.total_wells+1): for iii in range(1, proceduretype.total_wells+1):
row, column = proceduretype.ranked_plate[iii] row, column = proceduretype.ranked_plate[iii]
sample = next((item for item in ranked_samples if item['submission_rank']==iii), sample = next((item for item in ranked_samples if item['submission_rank']==iii),
dict(well_id=f"blank_{iii}", sample_id="", row=row, column=column, submission_rank=iii, background_color="#ffffff") dict(well_id=f"blank_{iii}", sample_id="", row=row, column=column, submission_rank=iii, background_color="#ffffff", enabled=False)
) )
padded_list.append(sample) padded_list.append(sample)
# logger.debug(f"Final padded list:\n{pformat(list(sorted(padded_list, key=itemgetter('submission_rank'))))}") # logger.debug(f"Final padded list:\n{pformat(list(sorted(padded_list, key=itemgetter('submission_rank'))))}")

View File

@@ -9,6 +9,8 @@ from dateutil.parser import parse
from dateutil.parser import ParserError from dateutil.parser import ParserError
from typing import List, Tuple, Literal from typing import List, Tuple, Literal
from types import GeneratorType from types import GeneratorType
import backend
from . import RSLNamer from . import RSLNamer
from pathlib import Path from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone
@@ -31,7 +33,11 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
def prevalidate(cls, data): def prevalidate(cls, data):
sql_fields = [k for k, v in cls._sql_object.__dict__.items() if isinstance(v, InstrumentedAttribute)] sql_fields = [k for k, v in cls._sql_object.__dict__.items() if isinstance(v, InstrumentedAttribute)]
output = {} output = {}
for key, value in data.items(): try:
items = data.items()
except AttributeError:
return data
for key, value in items:
new_key = key.replace("_", "") new_key = key.replace("_", "")
if new_key in sql_fields: if new_key in sql_fields:
output[new_key] = value output[new_key] = value
@@ -104,11 +110,14 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
def to_sql(self): def to_sql(self):
dicto = self.improved_dict(dictionaries=False) dicto = self.improved_dict(dictionaries=False)
logger.debug(f"Dicto: {dicto}") logger.debug(f"Dicto: {dicto}")
sql, _ = self._sql_object().query_or_create(**dicto) # sql, new = self._sql_object().query_or_create(**dicto)
sql, new = self._sql_object.query_or_create(**dicto)
if new:
logger.warning(f"Creating new {self._sql_object} with values:\n{pformat(dicto)}")
return sql return sql
class PydReagent(BaseModel): class PydReagent(PydBaseClass):
lot: str | None lot: str | None
reagentrole: str | None reagentrole: str | None
expiry: date | datetime | Literal['NA'] | None = Field(default=None, validate_default=True) expiry: date | datetime | Literal['NA'] | None = Field(default=None, validate_default=True)
@@ -269,6 +278,7 @@ class PydSample(PydBaseClass):
value = row_keys[value] value = row_keys[value]
return value return value
class PydTips(BaseModel): class PydTips(BaseModel):
name: str name: str
lot: str | None = Field(default=None) lot: str | None = Field(default=None)
@@ -277,6 +287,13 @@ class PydTips(BaseModel):
@field_validator('tiprole', mode='before') @field_validator('tiprole', mode='before')
@classmethod @classmethod
def get_role_name(cls, value): def get_role_name(cls, value):
if isinstance(value, list):
output = []
for tiprole in value:
if isinstance(tiprole, TipRole):
tiprole = tiprole.name
return tiprole
value = output
if isinstance(value, TipRole): if isinstance(value, TipRole):
value = value.name value = value.name
return value return value
@@ -304,13 +321,21 @@ class PydEquipment(BaseModel, extra='ignore'):
asset_number: str asset_number: str
name: str name: str
nickname: str | None nickname: str | None
process: List[str] | None # process: List[dict] | None
equipmentrole: str | None process: PydProcess | None
tips: List[PydTips] | None = Field(default=None) equipmentrole: str | PydEquipmentRole | None
tips: List[PydTips] | None = Field(default=[])
@field_validator('equipmentrole', mode='before') @field_validator('equipmentrole', mode='before')
@classmethod @classmethod
def get_role_name(cls, value): def get_role_name(cls, value):
match value:
case list():
value = value[0]
case GeneratorType():
value = next(value)
case _:
pass
if isinstance(value, EquipmentRole): if isinstance(value, EquipmentRole):
value = value.name value = value.name
return value return value
@@ -325,12 +350,24 @@ class PydEquipment(BaseModel, extra='ignore'):
value = convert_nans_to_nones(value) value = convert_nans_to_nones(value)
if not value: if not value:
value = [''] value = ['']
logger.debug(value)
try: try:
value = [item.strip() for item in value] # value = [item.strip() for item in value]
value = next((PydProcess(**process.details_dict()) for process in value))
except AttributeError: except AttributeError:
pass pass
return value return value
@field_validator('tips', mode='before')
@classmethod
def tips_to_pydantic(cls, value):
output = []
for tips in value:
if isinstance(tips, Tips):
tips = tips.to_pydantic()
output.append(tips)
return output
@report_result @report_result
def to_sql(self, procedure: Procedure | str = None, kittype: KitType | str = None) -> Tuple[ def to_sql(self, procedure: Procedure | str = None, kittype: KitType | str = None) -> Tuple[
Equipment, ProcedureEquipmentAssociation]: Equipment, ProcedureEquipmentAssociation]:
@@ -357,7 +394,7 @@ class PydEquipment(BaseModel, extra='ignore'):
# NOTE: Need to make sure the same association is not added to the procedure # NOTE: Need to make sure the same association is not added to the procedure
try: try:
assoc, new = ProcedureEquipmentAssociation.query_or_create(equipment=equipment, procedure=procedure, assoc, new = ProcedureEquipmentAssociation.query_or_create(equipment=equipment, procedure=procedure,
equipmentrole=self.equipmentrole, limit=1) equipmentrole=self.equipmentrole, limit=1)
except TypeError as e: except TypeError as e:
logger.error(f"Couldn't get association due to {e}, returning...") logger.error(f"Couldn't get association due to {e}, returning...")
return None, None return None, None
@@ -1282,8 +1319,14 @@ class PydProcess(BaseModel, extra="allow"):
@classmethod @classmethod
def enforce_list(cls, value): def enforce_list(cls, value):
if not isinstance(value, list): if not isinstance(value, list):
return [value] value = [value]
return value output = []
for v in value:
if issubclass(v.__class__, BaseClass):
output.append(v.name)
else:
output.append(v)
return output
@report_result @report_result
def to_sql(self): def to_sql(self):
@@ -1356,7 +1399,28 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
plate_map: str | None = Field(default=None) plate_map: str | None = Field(default=None)
reagent: list | None = Field(default=[]) reagent: list | None = Field(default=[])
reagentrole: dict | None = Field(default={}, validate_default=True) reagentrole: dict | None = Field(default={}, validate_default=True)
samples: List[PydSample] = Field(default=[]) sample: List[PydSample] = Field(default=[])
equipment: List[PydEquipment] = Field(default=[])
results: List[PydResults] | List[dict] = Field(default=[])
@field_validator("name", "technician", "kittype", mode="before")
@classmethod
def convert_to_dict(cls, value):
if isinstance(value, str):
value = dict(value=value, missing=False)
return value
@field_validator("proceduretype", mode="before")
@classmethod
def lookup_proceduretype(cls, value):
match value:
case dict():
value = ProcedureType.query(name=value['name'])
case str():
value = ProcedureType.query(name=value)
case _:
pass
return value
@field_validator("name") @field_validator("name")
@classmethod @classmethod
@@ -1378,25 +1442,41 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
@classmethod @classmethod
def rescue_possible_kits(cls, value, values): def rescue_possible_kits(cls, value, values):
if not value: if not value:
if values.data['proceduretype']: try:
value = [kittype.name for kittype in values.data['proceduretype'].kittype] if values.data['proceduretype']:
value = [kittype.__dict__['name'] for kittype in values.data['proceduretype'].kittype]
except KeyError:
pass
return value return value
@field_validator("name", "technician", "kittype") @field_validator("name", "technician", "kittype")
@classmethod @classmethod
def set_colour(cls, value): def set_colour(cls, value):
if value["missing"]: try:
value["colour"] = "FE441D" if value["missing"]:
else: value["colour"] = "FE441D"
value["colour"] = "6ffe1d" else:
value["colour"] = "6ffe1d"
except KeyError:
pass
return value return value
@field_validator("reagentrole") @field_validator("reagentrole")
@classmethod @classmethod
def rescue_reagentrole(cls, value, values): def rescue_reagentrole(cls, value, values):
if not value: if not value:
if values.data['kittype']['value'] != cls.model_fields['kittype'].default['value']: match values.data['kittype']:
kittype = KitType.query(name=values.data['kittype']['value']) case dict():
if "value" in values.data['kittype'].keys():
roi = values.data['kittype']['value']
elif "name" in values.data['kittype'].keys():
roi = values.data['kittype']['name']
else:
raise KeyError(f"Couldn't find kittype name in the dictionary: {values.data['kittype']}")
case str():
roi = values.data['kittype']
if roi != cls.model_fields['kittype'].default['value']:
kittype = KitType.query(name=roi)
value = {item.name: item.reagent for item in kittype.reagentrole} value = {item.name: item.reagent for item in kittype.reagentrole}
return value return value
@@ -1413,6 +1493,19 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
self.kittype['value'] = kittype self.kittype['value'] = kittype
self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype))) self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype)))
def update_kittype_equipmentroles(self, kittype: str | KitType):
if kittype == self.__class__.model_fields['kittype'].default['value']:
return
if isinstance(kittype, str):
kittype_obj = KitType.query(name=kittype)
try:
self.equipment = {item.name: item.get_reagents(kittype=kittype_obj) for item in
kittype_obj.get_reagents(proceduretype=self.proceduretype)}
except AttributeError:
self.reagentrole = {}
self.kittype['value'] = kittype
self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype)))
def update_samples(self, sample_list: List[dict]): def update_samples(self, sample_list: List[dict]):
logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}") logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}")
for sample_dict in sample_list: for sample_dict in sample_list:
@@ -1425,7 +1518,8 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
(item for item in self.samples if item.sample_id.upper() == sample_dict['sample_id'].upper())) (item for item in self.samples if item.sample_id.upper() == sample_dict['sample_id'].upper()))
except StopIteration: except StopIteration:
# NOTE: Code to check for added controls. # NOTE: Code to check for added controls.
logger.debug(f"Sample not found by name: {sample_dict['sample_id']}, checking row {row} column {column}") logger.debug(
f"Sample not found by name: {sample_dict['sample_id']}, checking row {row} column {column}")
try: try:
sample = next( sample = next(
(item for item in self.samples if item.row == row and item.column == column)) (item for item in self.samples if item.row == row and item.column == column))
@@ -1441,7 +1535,12 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
def to_sql(self): def to_sql(self):
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
# results = []
# for result in self.results:
# result, _ = result.to_sql()
sql = super().to_sql() sql = super().to_sql()
logger.debug(f"Initial PYD: {pformat(self.__dict__)}")
# sql.results = [result.to_sql() for result in self.results]
if self.run: if self.run:
sql.run = self.run sql.run = self.run
if self.proceduretype: if self.proceduretype:
@@ -1450,27 +1549,37 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
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:
start_index = 1 start_index = 1
relevant_samples = [sample for sample in self.samples if not sample.sample_id.startswith("blank_") and not sample.sample_id == ""] relevant_samples = [sample for sample in self.sample if
not sample.sample_id.startswith("blank_") and not sample.sample_id == ""]
logger.debug(f"start index: {start_index}") logger.debug(f"start index: {start_index}")
assoc_id_range = range(start_index, start_index + len(relevant_samples)+1) assoc_id_range = range(start_index, start_index + len(relevant_samples) + 1)
logger.debug(f"Association id range: {assoc_id_range}") logger.debug(f"Association id range: {assoc_id_range}")
for iii, sample in enumerate(relevant_samples): for iii, sample in enumerate(relevant_samples):
sample_sql = sample.to_sql() sample_sql = sample.to_sql()
if sql.run: if sql.run:
if sample_sql not in sql.run.sample: if sample_sql not in sql.run.sample:
logger.debug(f"sample {sample_sql} not found in {sql.run.sample}") logger.debug(f"sample {sample_sql} not found in {sql.run.sample}")
run_assoc = RunSampleAssociation(sample=sample_sql, run=self.run, row=sample.row, column=sample.column) run_assoc = RunSampleAssociation(sample=sample_sql, run=self.run, row=sample.row,
column=sample.column)
else: else:
logger.debug(f"sample {sample_sql} found in {sql.run.sample}") logger.debug(f"sample {sample_sql} found in {sql.run.sample}")
proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql, row=sample.row, column=sample.column) if sample_sql not in sql.sample:
proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql,
row=sample.row, column=sample.column)
if self.kittype['value'] not in ["NA", None, ""]: if self.kittype['value'] not in ["NA", None, ""]:
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
for equipment in self.equipment:
equip = Equipment.query(name=equipment.name)
if equip not in sql.equipment:
equip_assoc = ProcedureEquipmentAssociation(equipment=equip, procedure=sql, equipmentrole=equip.equipmentrole[0])
process = equipment.process.to_sql()
equip_assoc.process = process
logger.debug(f"Output sql: {[pformat(item.__dict__) for item in sql.procedureequipmentassociation]}")
return sql, None return sql, None
class PydClientSubmission(PydBaseClass): class PydClientSubmission(PydBaseClass):
# sql_object: ClassVar = ClientSubmission # sql_object: ClassVar = ClientSubmission
@@ -1549,15 +1658,34 @@ class PydClientSubmission(PydBaseClass):
class PydResults(PydBaseClass, arbitrary_types_allowed=True): class PydResults(PydBaseClass, arbitrary_types_allowed=True):
results: dict = Field(default={}) results: dict = Field(default={})
results_type: str = Field(default="NA") results_type: str = Field(default="NA")
img: None | bytes = Field(default=None) img: None | bytes = Field(default=None)
parent: Procedure|ProcedureSampleAssociation|None = Field(default=None) parent: Procedure | ProcedureSampleAssociation | None = Field(default=None)
date_analyzed: datetime | None = Field(default=None)
@field_validator("date_analyzed")
@classmethod
def set_today(cls, value):
match value:
case str():
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
case datetime():
pass
case _:
value = datetime.now()
return value
def to_sql(self): def to_sql(self):
sql = Results(results_type=self.results_type, result=self.results) sql, _ = Results.query_or_create(results_type=self.results_type, result=self.results)
sql.image = self.img try:
check = sql.image
except FileNotFoundError:
check = False
if not check:
sql.image = self.img
if not sql.date_analyzed:
sql.date_analyzed = self.date_analyzed
match self.parent: match self.parent:
case ProcedureSampleAssociation(): case ProcedureSampleAssociation():
sql.sampleprocedureassociation = self.parent sql.sampleprocedureassociation = self.parent
@@ -1566,4 +1694,3 @@ class PydResults(PydBaseClass, arbitrary_types_allowed=True):
case _: case _:
logger.error("Improper association found.") logger.error("Improper association found.")
return sql return sql

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from pandas import ExcelWriter from pandas import ExcelWriter
from backend import Reagent, Sample, ClientSubmission, KitType, Run from backend.db.models import Reagent, Sample, ClientSubmission, KitType, Run
from tools import ( from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development under_development

View File

@@ -127,7 +127,7 @@ class RoleComboBox(QWidget):
logger.debug(f"Equip2: {equip2}") logger.debug(f"Equip2: {equip2}")
with QSignalBlocker(self.process) as blocker: with QSignalBlocker(self.process) as blocker:
self.process.clear() self.process.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.process]) self.process.addItems([item for item in equip2.process if item in self.role.process])
def update_tips(self): def update_tips(self):
""" """

View File

@@ -14,27 +14,28 @@ from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QGridLayout, QMenu, QDialogButtonBox from PyQt6.QtWidgets import QDialog, QGridLayout, QMenu, QDialogButtonBox
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.db.models import Run, ProcedureType from backend.db.models import Run, Procedure
from backend.validators import PydProcedure
from tools import jinja_template_loading, get_application_from_parent, render_details_template from tools import jinja_template_loading, get_application_from_parent, render_details_template
from backend.validators import PydProcedure
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class ProcedureCreation(QDialog): class ProcedureCreation(QDialog):
def __init__(self, parent, run: Run, proceduretype: ProcedureType): def __init__(self, parent, procedure: PydProcedure):
super().__init__(parent) super().__init__(parent)
self.run = run self.run = procedure.run
self.proceduretype = proceduretype self.procedure = procedure
self.setWindowTitle(f"New {proceduretype.name} for { run.rsl_plate_number }") self.proceduretype = procedure.proceduretype
self.created_procedure = self.proceduretype.construct_dummy_procedure(run=self.run) self.setWindowTitle(f"New {self.proceduretype.name} for { self.run.rsl_plate_number }")
self.created_procedure.update_kittype_reagentroles(kittype=self.created_procedure.possible_kits[0]) # self.created_procedure = self.proceduretype.construct_dummy_procedure(run=self.run)
self.created_procedure.samples = self.run.constuct_sample_dicts_for_proceduretype(proceduretype=self.proceduretype) self.procedure.update_kittype_reagentroles(kittype=self.procedure.possible_kits[0])
# self.created_procedure.samples = self.run.constuct_sample_dicts_for_proceduretype(proceduretype=self.proceduretype)
# logger.debug(f"Samples to map\n{pformat(self.created_procedure.samples)}") # logger.debug(f"Samples to map\n{pformat(self.created_procedure.samples)}")
self.plate_map = self.proceduretype.construct_plate_map(sample_dicts=self.created_procedure.samples) self.plate_map = self.proceduretype.construct_plate_map(sample_dicts=self.procedure.sample)
# logger.debug(f"Plate map: {self.plate_map}") # logger.debug(f"Plate map: {self.plate_map}")
# logger.debug(f"Created dummy: {self.created_procedure}") # logger.debug(f"Created dummy: {self.created_procedure}")
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -61,46 +62,56 @@ class ProcedureCreation(QDialog):
self.layout.addWidget(self.buttonBox, 11, 1, 1, 1) self.layout.addWidget(self.buttonBox, 11, 1, 1, 1)
def set_html(self): def set_html(self):
from .equipment_usage_2 import EquipmentUsage
proceduretype_dict = self.proceduretype.details_dict()
if self.procedure.equipment:
for equipmentrole in proceduretype_dict['equipment']:
# NOTE: Check if procedure equipment is present and move to head of the list if so.
try:
relevant_procedure_item = next((equipment for equipment in self.procedure.equipment if equipment.equipmentrole == equipmentrole['name']))
except StopIteration:
continue
item_in_er_list = next((equipment for equipment in equipmentrole['equipment'] if equipment['name'] == relevant_procedure_item.name))
equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(equipmentrole['equipment'].index(item_in_er_list)))
proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True)
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'],
js_in=["procedure_form", "grid_drag", "context_menu"], js_in=["procedure_form", "grid_drag", "context_menu"],
proceduretype=self.proceduretype.details_dict(), proceduretype=proceduretype_dict,
run=self.run.details_dict(), run=self.run.details_dict(),
procedure=self.created_procedure.__dict__, procedure=self.procedure.__dict__,
plate_map=self.plate_map plate_map=self.plate_map
) )
# with open("procedure.html", "w") as f:
# f.write(html)
self.webview.setHtml(html) self.webview.setHtml(html)
@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}")
attribute = getattr(self.created_procedure, key) attribute = getattr(self.procedure, key)
attribute['value'] = new_value.strip('\"') attribute['value'] = new_value.strip('\"')
@pyqtSlot(str, bool) @pyqtSlot(str, bool)
def check_toggle(self, key: str, ischecked: bool): def check_toggle(self, key: str, ischecked: bool):
# logger.debug(f"{key} is checked: {ischecked}") # logger.debug(f"{key} is checked: {ischecked}")
setattr(self.created_procedure, key, ischecked) setattr(self.procedure, key, ischecked)
@pyqtSlot(str) @pyqtSlot(str)
def update_kit(self, kittype): def update_kit(self, kittype):
self.created_procedure.update_kittype_reagentroles(kittype=kittype) self.procedure.update_kittype_reagentroles(kittype=kittype)
logger.debug({k: v for k, v in self.created_procedure.__dict__.items() if k != "plate_map"}) logger.debug({k: v for k, v in self.procedure.__dict__.items() if k != "plate_map"})
self.set_html() self.set_html()
@pyqtSlot(list) @pyqtSlot(list)
def rearrange_plate(self, sample_list: list): def rearrange_plate(self, sample_list: list):
self.created_procedure.update_samples(sample_list=sample_list) self.procedure.update_samples(sample_list=sample_list)
@pyqtSlot(str) @pyqtSlot(str)
def log(self, logtext: str): def log(self, logtext: str):
logger.debug(logtext) logger.debug(logtext)
def return_sql(self): def return_sql(self):
return self.created_procedure.to_sql() return self.procedure.to_sql()
# class ProcedureWebViewer(QWebEngineView): # class ProcedureWebViewer(QWebEngineView):

View File

@@ -179,3 +179,7 @@ ul.no-bullets {
text-decoration-color: red; text-decoration-color: red;
} }
.grid-container {
display: grid;
grid-auto-flow: column;
}

View File

@@ -0,0 +1,89 @@
const equipment_json = {{ proceduretype['equipment_json'] }};
window.addEventListener('load', function () {
equipment_json.forEach(startup);
})
function startup(equipmentrole) {
updateEquipmentChoices(equipmentrole);
var eq_dropdown = document.getElementById(equipmentrole.name);
eq_dropdown.addEventListener("change", function(event){
updateProcessChoices(equipmentrole);
updateBackend(equipmentrole);
});
var process_dropdown = document.getElementById(equipmentrole.name + "_process");
process_dropdown.addEventListener("change", function(event){
updateTipChoices(equipmentrole);
updateBackend(equipmentrole);
});
var tips_dropdown = document.getElementById(equipmentrole.name + "_tips");
tips_dropdown.addEventListener("change", function(event){
updateBackend(equipmentrole);
});
}
function updateEquipmentChoices(equipmentrole) {
console.log("Updating equipment choices.");
var dropdown_oi = document.getElementById(equipmentrole.name);
while (dropdown_oi.options.length > 0) {
dropdown_oi.remove(0);
}
dropdown_oi.json = equipmentrole;
for (let iii = 0; iii < equipmentrole.equipment.length; iii++) {
var opt = document.createElement('option');
opt.value = equipmentrole.equipment[iii].name;
opt.innerHTML = equipmentrole.equipment[iii].name;
dropdown_oi.appendChild(opt);
}
updateProcessChoices(equipmentrole);
}
function updateProcessChoices(equipmentrole) {
console.log("Updating process choices.");
var dropdown_oi = document.getElementById(equipmentrole.name + "_process");
while (dropdown_oi.options.length > 0) {
dropdown_oi.remove(0);
}
dropdown_oi.json = equipmentrole;
var equipment_name = document.getElementById(equipmentrole.name).value;
var equipment = equipmentrole.equipment.filter(function(x){ return x.name == equipment_name })[0];
for (let iii = 0; iii < equipment.processes.length; iii++) {
var opt = document.createElement('option');
opt.value = equipment.processes[iii].name;
opt.innerHTML = equipment.processes[iii].name;
dropdown_oi.appendChild(opt);
}
updateTipChoices(equipmentrole);
}
function updateTipChoices(equipmentrole) {
console.log("Updating tip choices.");
var dropdown_oi = document.getElementById(equipmentrole.name + "_tips");
dropdown_oi.innerHTML = "";
dropdown_oi.json = equipmentrole;
var equipment_name = document.getElementById(equipmentrole.name).value;
var process_name = document.getElementById(equipmentrole.name + "_process").value;
console.log(process_name);
var equipment = equipmentrole.equipment.filter(function(x){ return x.name == equipment_name })[0];
console.log(equipment);
var process = equipment.processes.filter(function(x){ return x.name == process_name })[0];
console.log(process);
for (let iii = 0; iii < process.tips.length; iii++) {
var opt = document.createElement('option');
opt.value = process.tips[iii];
opt.innerHTML = process.tips[iii];
dropdown_oi.appendChild(opt);
}
}
function updateBackend(equipmentrole) {
alert("Updating Backend");
var equipmentrole_name = equipmentrole.name
var dropdown_oi = document.getElementById(equipmentrole.name);
var equipment_name = dropdown_oi.value;
dropdown_oi = document.getElementById(equipmentrole.name + "_process");
var process_name = dropdown_oi.value;
dropdown_oi = document.getElementById(equipmentrole.name + "_tips");
var tips_name = dropdown_oi.value;
backend.update_equipment(equipmentrole_name, equipment_name, process_name, tips_name)
}

View File

@@ -28,24 +28,13 @@
<br><hr><br> <br><hr><br>
{% for key, value in procedure['reagentrole'].items() %} {% for key, value in procedure['reagentrole'].items() %}
<label for="{{ key }}">{{ key }}:</label><br> <label for="{{ key }}">{{ key }}:</label><br>
<datalist class="reagentrole dropdown" id="{{ key }}" name="{{ reagentrole }}"><br> <select class="reagentrole dropdown" id="{{ key }}" name="{{ key }}"><br>
{% for reagent in value %} {% for reagent in value %}
<option value="{{ reagent }}">{{ reagent }}</option> <option value="{{ reagent }}">{{ reagent }}</option>
{% endfor %} {% endfor %}
</datalist>
{% endfor %}
{% endif %}
{% if proceduretype['equipment'] %}
<br><hr><br>
{% for equipmentrole in proceduretype['equipment'] %}
<label for="{{ equipmentrole['name'] }}">{{ equipmentrole['name'] }}:</label><br>
<select class="equipmentrole dropdown" id="{{ equipmentrole['name'] }}" name="{{ equipmentrole['name'] }}"><br>
{% for equipment in equipmentrole['equipment'] %}
<option value="{{ equipment['name'] }}">{{ equipment['name'] }}</option>
{% endfor %}
</select> </select>
{% endfor %} {% endfor %}
{% endif%} {% endif %}
</form> </form>
</div> </div>
<div class="right"> <div class="right">
@@ -55,6 +44,9 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% with proceduretype=proceduretype, child=True %}
{% include "support/equipment_usage.html" %}
{% endwith %}
{% include 'support/context_menu.html' %} {% include 'support/context_menu.html' %}
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,141 @@
{% if not child %}
<html lang="en">
<head>
{% block head %}
<meta charset="UTF-8">
{% if css %}
<style>
{% for c in css %}
{{ c }}
{% endfor %}
</style>
{% endif %}
{% endblock %}
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
</head>
<body>
{% endif %}
<h1><u>Equipment</u></h1>
<br><hr><br>
{% for equipmentrole in proceduretype['equipment_json'] %}
<div class="grid-container">
<div>
<label for="{{ equipmentrole['name'] }}">{{ equipmentrole['name'] }}:</label><br>
<select class="equipmentrole dropdown" id="{{ equipmentrole['name'] }}" name="{{ equipmentrole['name'] }}">
</select>
</div>
<div>
<label for="{{ equipmentrole['name'] }}_process">Process:</label><br>
<select class="process dropdown" id="{{ equipmentrole['name'] }}_process" name="{{ equipmentrole['name'] }}_process">
</select>
</div>
<div>
<label for="{{ equipmentrole['name'] }}_tips">Tips:</label><br>
<select class="tips dropdown" id="{{ equipmentrole['name'] }}_tips" name="{{ equipmentrole['name'] }}_tips">
</select>
</div>
</div>
{% endfor %}
{% if not child %}
{% for j in js%}
<script>
{{ j }}
</script>
{% endfor %}
{% endif %}
<script>
const equipment_json = {{ proceduretype['equipment_json'] }};
window.addEventListener('load', function () {
equipment_json.forEach(startup);
})
function startup(equipmentrole) {
updateEquipmentChoices(equipmentrole);
var eq_dropdown = document.getElementById(equipmentrole.name);
eq_dropdown.addEventListener("change", function(event){
updateProcessChoices(equipmentrole);
updateBackend(equipmentrole);
});
var process_dropdown = document.getElementById(equipmentrole.name + "_process");
process_dropdown.addEventListener("change", function(event){
updateTipChoices(equipmentrole);
updateBackend(equipmentrole);
});
var tips_dropdown = document.getElementById(equipmentrole.name + "_tips");
tips_dropdown.addEventListener("change", function(event){
updateBackend(equipmentrole);
});
updateBackend(equipmentrole);
}
function updateEquipmentChoices(equipmentrole) {
console.log("Updating equipment choices.");
var dropdown_oi = document.getElementById(equipmentrole.name);
while (dropdown_oi.options.length > 0) {
dropdown_oi.remove(0);
}
dropdown_oi.json = equipmentrole;
for (let iii = 0; iii < equipmentrole.equipment.length; iii++) {
var opt = document.createElement('option');
opt.value = equipmentrole.equipment[iii].name;
opt.innerHTML = equipmentrole.equipment[iii].name;
dropdown_oi.appendChild(opt);
}
updateProcessChoices(equipmentrole);
}
function updateProcessChoices(equipmentrole) {
console.log("Updating process choices.");
var dropdown_oi = document.getElementById(equipmentrole.name + "_process");
while (dropdown_oi.options.length > 0) {
dropdown_oi.remove(0);
}
dropdown_oi.json = equipmentrole;
var equipment_name = document.getElementById(equipmentrole.name).value;
var equipment = equipmentrole.equipment.filter(function(x){ return x.name == equipment_name })[0];
for (let iii = 0; iii < equipment.processes.length; iii++) {
var opt = document.createElement('option');
opt.value = equipment.processes[iii].name;
opt.innerHTML = equipment.processes[iii].name;
dropdown_oi.appendChild(opt);
}
updateTipChoices(equipmentrole);
}
function updateTipChoices(equipmentrole) {
console.log("Updating tip choices.");
var dropdown_oi = document.getElementById(equipmentrole.name + "_tips");
dropdown_oi.innerHTML = "";
dropdown_oi.json = equipmentrole;
var equipment_name = document.getElementById(equipmentrole.name).value;
var process_name = document.getElementById(equipmentrole.name + "_process").value;
console.log(process_name);
var equipment = equipmentrole.equipment.filter(function(x){ return x.name == equipment_name })[0];
console.log(equipment);
var process = equipment.processes.filter(function(x){ return x.name == process_name })[0];
console.log(process);
for (let iii = 0; iii < process.tips.length; iii++) {
var opt = document.createElement('option');
opt.value = process.tips[iii];
opt.innerHTML = process.tips[iii];
dropdown_oi.appendChild(opt);
}
}
function updateBackend(equipmentrole) {
var equipmentrole_name = equipmentrole.name
var dropdown_oi = document.getElementById(equipmentrole.name);
var equipment_name = dropdown_oi.value;
dropdown_oi = document.getElementById(equipmentrole.name + "_process");
var process_name = dropdown_oi.value;
dropdown_oi = document.getElementById(equipmentrole.name + "_tips");
var tips_name = dropdown_oi.value;
backend.update_equipment(equipmentrole_name, equipment_name, process_name, tips_name)
}
</script>