Update of Reagent usage ui for addition of new reagents.

This commit is contained in:
lwark
2025-07-02 13:42:40 -05:00
parent fd63608744
commit c4c330b90c
8 changed files with 154 additions and 83 deletions

View File

@@ -603,7 +603,7 @@ class BaseClass(Base):
pyd = getattr(pydant, pyd_model_name)
except AttributeError:
raise AttributeError(f"Could not get pydantic class {pyd_model_name}")
return pyd(**self.details_dict())
return pyd(**self.details_dict(**kwargs))
def show_details(self, obj):
logger.debug("Show Details")

View File

@@ -5,13 +5,11 @@ from __future__ import annotations
import zipfile, logging, re
from operator import itemgetter
from pprint import pformat
import numpy as np
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
jinja_template_loading, flatten_list
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
@@ -562,7 +560,8 @@ class ReagentRole(BaseClass):
def get_reagents(self, kittype: str | KitType | None = None):
if not kittype:
return [f"{reagent.name} - {reagent.lot}" for reagent in self.reagent]
# return [f"{reagent.name} - {reagent.lot} - {reagent.expiry}" for reagent in self.reagent]
return [reagent.to_pydantic() for reagent in self.reagent]
if isinstance(kittype, str):
kittype = KitType.query(name=kittype)
assoc = next((item for item in self.reagentrolekittypeassociation if item.kittype == kittype), None)
@@ -571,7 +570,8 @@ class ReagentRole(BaseClass):
last_used = Reagent.query(name=assoc.last_used)
if last_used:
reagents.insert(0, reagents.pop(reagents.index(last_used)))
return [f"{reagent.name} - {reagent.lot}" for reagent in reagents]
# return [f"{reagent.name} - {reagent.lot} - {reagent.expiry}" for reagent in reagents]
return [reagent.to_pydantic(reagentrole=self.name) for reagent in reagents]
class Reagent(BaseClass, LogMixin):
@@ -680,24 +680,24 @@ class Reagent(BaseClass, LogMixin):
report.add_result(Result(msg=f"Updating last used {rt} was not performed.", status="Information"))
return report
@classmethod
def query_or_create(cls, **kwargs) -> Reagent:
from backend.validators.pydant import PydReagent
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
if "reagentrole" not in kwargs:
try:
kwargs['reagentrole'] = kwargs['name']
except KeyError:
pass
instance = PydReagent(**kwargs)
new = True
instance = instance.to_sql()
logger.info(f"Instance from query or create: {instance}")
return instance, new
# @classmethod
# def query_or_create(cls, **kwargs) -> Reagent:
# from backend.validators.pydant import PydReagent
# new = False
# disallowed = ['expiry']
# sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
# instance = cls.query(**sanitized_kwargs)
# if not instance or isinstance(instance, list):
# if "reagentrole" not in kwargs:
# try:
# kwargs['reagentrole'] = kwargs['name']
# except KeyError:
# pass
# instance = PydReagent(**kwargs)
# new = True
# instance = instance.to_sql()
# logger.info(f"Instance from query or create: {instance}")
# return instance, new
@classmethod
@setup_lookup
@@ -800,6 +800,12 @@ class Reagent(BaseClass, LogMixin):
expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically"
)
def details_dict(self, reagentrole:str|None=None, **kwargs):
output = super().details_dict()
if reagentrole:
output['reagentrole'] = reagentrole
return output
class Discount(BaseClass):
"""
@@ -1278,7 +1284,7 @@ class ProcedureType(BaseClass):
if self.plate_rows == 0 or self.plate_columns == 0:
return "<br/>"
sample_dicts = self.pad_sample_dicts(sample_dicts=sample_dicts)
logger.debug(f"Sample dicts: {pformat(sample_dicts)}")
# logger.debug(f"Sample dicts: {pformat(sample_dicts)}")
vw = round((-0.07 * len(sample_dicts)) + 12.2, 1)
# 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
@@ -1291,14 +1297,14 @@ class ProcedureType(BaseClass):
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}")
# 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}")
# logger.debug(f"Appending {sample} at row {row}, column {column}")
return output
@@ -1453,6 +1459,8 @@ class Procedure(BaseClass):
dlg = ProcedureCreation(parent=obj, procedure=self.to_pydantic(), edit=True)
if dlg.exec():
logger.debug("Edited")
sql, _ = dlg.return_sql()
sql.save()
def add_comment(self, obj):
logger.debug("Add Comment!")

View File

@@ -183,7 +183,7 @@ class PydReagent(PydBaseClass):
case _:
return convert_nans_to_nones(str(value))
if value is None:
value = date.today()
value = datetime.combine(date.today(), datetime.max.time())
return value
@field_validator("expiry")
@@ -201,6 +201,11 @@ class PydReagent(PydBaseClass):
else:
return values.data['reagentrole'].strip()
# @field_validator("reagentrole", mode="before")
# @classmethod
# def rescue_reagentrole(cls, value):
# if
def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
@@ -226,23 +231,26 @@ class PydReagent(PydBaseClass):
report = Report()
if self.model_extra is not None:
self.__dict__.update(self.model_extra)
reagent = Reagent.query(lot=self.lot, name=self.name)
reagent, new = Reagent.query_or_create(lot=self.lot, name=self.name)
# logger.debug(f"Reagent: {reagent}")
if reagent is None:
reagent = Reagent()
for key, value in self.__dict__.items():
if isinstance(value, dict):
value = value['value']
# NOTE: reagent method sets fields based on keys in dictionary
reagent.set_attribute(key, value)
if procedure is not None and reagent not in procedure.reagents:
assoc = ProcedureReagentAssociation(reagent=reagent, procedure=procedure)
assoc.comments = self.comment
else:
assoc = None
else:
if submission is not None and reagent not in submission.reagents:
submission.update_reagentassoc(reagent=reagent, role=self.role)
# if reagent is None:
# reagent = Reagent()
# for key, value in self.__dict__.items():
# if isinstance(value, dict):
# if key == "misc_info":
# value = value
# else:
# value = value['value']
# # NOTE: reagent method sets fields based on keys in dictionary
# reagent.set_attribute(key, value)
# if procedure is not None and reagent not in procedure.reagents:
# assoc = ProcedureReagentAssociation(reagent=reagent, procedure=procedure)
# assoc.comments = self.comment
# else:
# assoc = None
# else:
# if submission is not None and reagent not in submission.reagents:
# submission.update_reagentassoc(reagent=reagent, role=self.role)
return reagent, report
@@ -1486,8 +1494,10 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
if isinstance(kittype, str):
kittype_obj = KitType.query(name=kittype)
try:
self.reagentrole = {item.name: item.get_reagents(kittype=kittype_obj) + ["New"] for item in
kittype_obj.get_reagents(proceduretype=self.proceduretype)}
self.reagentrole = {
item.name: item.get_reagents(kittype=kittype_obj) + [PydReagent(name="--New--", lot="", reagentrole="")]
for item in
kittype_obj.get_reagents(proceduretype=self.proceduretype)}
except AttributeError:
self.reagentrole = {}
@@ -1511,19 +1521,19 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}")
for sample_dict in sample_list:
if sample_dict['sample_id'].startswith("blank_"):
continue
sample_dict['sample_id'] = ""
row, column = self.proceduretype.ranked_plate[sample_dict['index']]
logger.debug(f"Row: {row}, Column: {column}")
try:
sample = next(
(item for item in self.samples if item.sample_id.upper() == sample_dict['sample_id'].upper()))
(item for item in self.sample if item.sample_id.upper() == sample_dict['sample_id'].upper()))
except StopIteration:
# NOTE: Code to check for added controls.
logger.debug(
f"Sample not found by name: {sample_dict['sample_id']}, checking row {row} column {column}")
try:
sample = next(
(item for item in self.samples if item.row == row and item.column == column))
(item for item in self.sample if item.row == row and item.column == column))
except StopIteration:
logger.error(f"Couldn't find sample: {pformat(sample_dict)}")
continue
@@ -1532,7 +1542,23 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
sample.well_id = sample_dict['sample_id']
sample.row = row
sample.column = column
logger.debug(f"Updated samples:\n{pformat(self.samples)}")
# logger.debug(f"Updated samples:\n{pformat(self.sample)}")
def update_reagents(self, reagentrole: str, name: str, lot: str, expiry: str):
removable = next((item for item in self.reagent if item.reagentrole == reagentrole), None)
if removable:
idx = self.reagent.index(removable)
self.reagent.remove(removable)
else:
idx = 0
insertable = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
self.reagent.insert(idx, insertable)
logger.debug(self.reagent)
@classmethod
def update_new_reagents(cls, reagent: PydReagent):
reg = reagent.to_sql()
reg.save()
def to_sql(self):
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
@@ -1540,12 +1566,24 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
# for result in self.results:
# result, _ = result.to_sql()
sql = super().to_sql()
logger.debug(f"Initial PYD: {pformat(self.__dict__)}")
# logger.debug(f"Initial PYD: {pformat(self.__dict__)}")
# sql.results = [result.to_sql() for result in self.results]
if self.run:
sql.run = self.run
if self.proceduretype:
sql.proceduretype = self.proceduretype
# Note: convert any new reagents to sql and save
for reagentrole, reagents in self.reagentrole.items():
for reagent in reagents:
if not reagent.lot or reagent.name == "--New--":
continue
self.update_new_reagents(reagent)
# NOTE: reset reagent associations.
sql.procedurereagentassociation = []
for reagent in self.reagent:
reagent = reagent.to_sql()
if reagent not in sql.reagent:
reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql)
try:
start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1
except ValueError:
@@ -1571,13 +1609,15 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
kittype = KitType.query(name=self.kittype['value'], limit=1)
if kittype:
sql.kittype = kittype
logger.debug(self.reagent)
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])
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]}")
# logger.debug(f"Output sql: {[pformat(item.__dict__) for item in sql.procedureequipmentassociation]}")
return sql, None

View File

@@ -3,6 +3,7 @@
"""
from __future__ import annotations
import datetime
import os
import sys, logging
from pathlib import Path
@@ -122,6 +123,24 @@ class ProcedureCreation(QDialog):
def log(self, logtext: str):
logger.debug(logtext)
@pyqtSlot(str, str, str, str)
def add_new_reagent(self, reagentrole: str, name: str, lot: str, expiry: str):
from backend.validators.pydant import PydReagent
expiry = datetime.datetime.strptime(expiry, "%Y-%m-%d")
pyd = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
logger.debug(pyd)
self.procedure.reagentrole[reagentrole].insert(0, pyd)
logger.debug(pformat(self.procedure.__dict__))
self.set_html()
@pyqtSlot(str, str)
def update_reagent(self, reagentrole:str, name_lot_expiry:str):
try:
name, lot, expiry = name_lot_expiry.split(" - ")
except ValueError:
return
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
def return_sql(self):
return self.procedure.to_sql()

View File

@@ -296,7 +296,7 @@ class SubmissionsTree(QTreeView):
self.setAlternatingRowColors(True)
self.setIndentation(20)
self.setItemsExpandable(True)
self.expanded.connect(self.expand_item)
# self.expanded.connect(self.expand_item)
for ii in range(2):
self.resizeColumnToContents(ii)

View File

@@ -32,24 +32,24 @@ gridContainer.addEventListener("drop", (e) => {
targetItem !== draggedItem //&&
//targetItem.classList.contains("well")
) {
backend.log(targetItem.id);
// backend.log(targetItem.id);
const draggedIndex = [...gridContainer.children].indexOf(draggedItem);
const targetIndex = [...gridContainer.children].indexOf(targetItem);
if (draggedIndex < targetIndex) {
backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Lesser");
// backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Lesser");
gridContainer.insertBefore(draggedItem, targetItem.nextSibling);
} else {
backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Greater");
// backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Greater");
gridContainer.insertBefore(draggedItem, targetItem);
}
// output = [];
// fullGrid = [...gridContainer.children];
// fullGrid.forEach(function(item, index) {
// output.push({sample_id: item.id, index: index + 1})
// });
// backend.rearrange_plate(output);
rearrange_plate();
output = [];
fullGrid = [...gridContainer.children];
fullGrid.forEach(function(item, index) {
output.push({sample_id: item.id, index: index + 1})
});
backend.rearrange_plate(output);
// rearrange_plate();
}
});

View File

@@ -21,45 +21,39 @@ for(let i = 0; i < formtexts.length; i++) {
})
};
var changed_it = new Event('change');
var reagentRoles = document.getElementsByClassName("reagentrole");
for(let i = 0; i < reagentRoles.length; i++) {
reagentRoles[i].addEventListener("change", function() {
if (reagentRoles[i].value === "New") {
if (reagentRoles[i].value.includes("--New--")) {
var br = document.createElement("br");
var new_reg = document.getElementById("new_" + reagentRoles[i].id);
console.log(new_reg.id);
var new_form = document.createElement("form");
new_form.setAttribute("class", "new_reagent_form")
new_form.setAttribute("id", reagentRoles[i].id + "_addition")
var rr_name = document.createElement("input");
rr_name.setAttribute("type", "text");
rr_name.setAttribute("id", "new_" + reagentRoles[i].id + "_name");
var rr_name_label = document.createElement("label");
rr_name_label.setAttribute("for", "new_" + reagentRoles[i].id + "_name");
rr_name_label.innerHTML = "Name:";
var rr_lot = document.createElement("input");
rr_lot.setAttribute("type", "text");
rr_lot.setAttribute("id", "new_" + reagentRoles[i].id + "_lot");
var rr_lot_label = document.createElement("label");
rr_lot_label.setAttribute("for", "new_" + reagentRoles[i].id + "_lot");
rr_lot_label.innerHTML = "Lot:";
var rr_expiry = document.createElement("input");
rr_expiry.setAttribute("type", "date");
rr_expiry.setAttribute("id", "new_" + reagentRoles[i].id + "_expiry");
var rr_expiry_label = document.createElement("label");
rr_expiry_label.setAttribute("for", "new_" + reagentRoles[i].id + "_expiry");
rr_expiry_label.innerHTML = "Expiry:";
var submit_btn = document.createElement("input");
submit_btn.setAttribute("type", "submit");
submit_btn.setAttribute("value", "Submit");
new_form.appendChild(br.cloneNode());
new_form.appendChild(rr_name_label);
new_form.appendChild(rr_name);
@@ -72,19 +66,27 @@ for(let i = 0; i < reagentRoles.length; i++) {
new_form.appendChild(br.cloneNode());
new_form.appendChild(submit_btn);
new_form.appendChild(br.cloneNode());
new_form.onsubmit = function(event) {
event.preventDefault();
alert(reagentRoles[i].id);
name = document.getElementById("new_" + reagentRoles[i].id + "_name").value;
lot = document.getElementById("new_" + reagentRoles[i].id + "_lot").value;
expiry = document.getElementById("new_" + reagentRoles[i].id + "_expiry").value;
alert("Submitting: " + name + ", " + lot);
backend.log(name + " " + lot + " " + expiry);
backend.add_new_reagent(reagentRoles[i].id, name, lot, expiry);
new_form.remove();
reagentRoles[i].dispatchEvent(changed_it);
}
new_reg.appendChild(new_form);
} else {
newregform = document.getElementById(reagentRoles[i].id + "_addition");
newregform.remove();
backend.update_reagent(reagentRoles[i].id, reagentRoles[i].value)
}
})
});
};
window.onload = function() {
for(let i = 0; i < reagentRoles.length; i++) {
backend.update_reagent(reagentRoles[i].id, reagentRoles[i].value);
}
}

View File

@@ -31,12 +31,14 @@
{% endfor %}
</select><br>
{% if procedure['reagentrole'] %}
<br><hr><br>
<br>
<h1><u>Reagents</u></h1>
<hr>
{% for key, value in procedure['reagentrole'].items() %}
<label for="{{ key }}">{{ key }}:</label><br>
<select class="reagentrole dropdown" id="{{ key }}" name="{{ key }}"><br>
{% for reagent in value %}
<option value="{{ reagent }}">{{ reagent }}</option>
<option value="{{ reagent.name }} {% if reagent.lot %}- {{ reagent.lot }} - {{ reagent.expiry.date() }}{% endif %}">{{ reagent.name }} {% if reagent.lot %}- {{ reagent.lot }} - {{ reagent.expiry.date() }}{% endif %}</option>
{% endfor %}
</select>
<div class="new_reagent" id="new_{{ key }}"></div>