Update of Reagent usage ui

This commit is contained in:
lwark
2025-06-30 10:35:53 -05:00
parent 054345e3e1
commit fd63608744
11 changed files with 446 additions and 27 deletions

View File

@@ -1450,7 +1450,7 @@ class Procedure(BaseClass):
def edit(self, obj):
from frontend.widgets.procedure_creation import ProcedureCreation
logger.debug("Edit!")
dlg = ProcedureCreation(parent=obj, procedure=self.to_pydantic())
dlg = ProcedureCreation(parent=obj, procedure=self.to_pydantic(), edit=True)
if dlg.exec():
logger.debug("Edited")

View File

@@ -1486,10 +1486,11 @@ 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) for item in
self.reagentrole = {item.name: item.get_reagents(kittype=kittype_obj) + ["New"] 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)))

View File

@@ -0,0 +1,129 @@
'''
Creates forms that the user can enter equipment info into.
'''
from pprint import pformat
from PyQt6.QtCore import Qt, QSignalBlocker, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
)
from backend.db.models import Equipment, Run, Process, Procedure, Tips
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips, PydProcedure
import logging
from typing import Generator
from tools import get_application_from_parent, render_details_template, flatten_list
logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, procedure: PydProcedure):
super().__init__(parent)
self.procedure = procedure
self.setWindowTitle(f"Equipment Checklist - {procedure.name}")
self.used_equipment = self.procedure.equipment
self.kit = self.procedure.kittype
self.opt_equipment = procedure.proceduretype.get_equipment()
self.layout = QVBoxLayout()
self.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self)
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.webview.setMinimumSize(1200, 800)
self.webview.setMaximumWidth(1200)
# NOTE: Decide if exporting should be allowed.
# self.webview.loadFinished.connect(self.activate_export)
self.layout = QGridLayout()
# NOTE: button to export a pdf version
self.layout.addWidget(self.webview, 1, 0, 10, 10)
self.setLayout(self.layout)
self.setFixedWidth(self.webview.width() + 20)
# NOTE: setup channel
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
html = self.construct_html(procedure=procedure)
self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.buttonBox, 11, 1, 1, 1)
@classmethod
def construct_html(cls, procedure: PydProcedure, child: bool = False):
proceduretype = procedure.proceduretype
proceduretype_dict = proceduretype.details_dict()
run = procedure.run
proceduretype_dict['equipment_json'] = flatten_list([item['equipment_json'] for item in proceduretype_dict['equipment']])
# proceduretype_dict['equipment_json'] = [
# {'name': 'Liquid Handler', 'equipment': [
# {'name': 'Other', 'asset_number': 'XXX', 'processes': [
# {'name': 'Trust Me', 'tips': ['Blah']},
# {'name': 'No Me', 'tips': ['Blah', 'Crane']}
# ]
# },
# {'name': 'Biomek', 'asset_number': '5015530', 'processes': [
# {'name': 'Sample Addition', 'tips': ['Axygen 20uL']
# }
# ]
# }
# ]
# }
# ]
# if 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 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)))
html = render_details_template(
template_name="support/equipment_usage",
css_in=[],
js_in=[],
proceduretype=proceduretype_dict,
run=run.details_dict(),
procedure=procedure.__dict__,
child=child
)
return html
@pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
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}")
def save_procedure(self):
sql, _ = self.procedure.to_sql()
logger.debug(pformat(sql.__dict__))
# import pickle
# with open("sql.pickle", "wb") as f:
# pickle.dump(sql, f)
# with open("pyd.pickle", "wb") as f:
# pickle.dump(self.procedure, f)
sql.save()

View File

@@ -14,19 +14,20 @@ from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QGridLayout, QMenu, QDialogButtonBox
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
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
logger = logging.getLogger(f"submissions.{__name__}")
class ProcedureCreation(QDialog):
def __init__(self, parent, procedure: PydProcedure):
def __init__(self, parent, procedure: PydProcedure, edit: bool = False):
super().__init__(parent)
self.edit = edit
self.run = procedure.run
self.procedure = procedure
self.proceduretype = procedure.proceduretype
@@ -63,16 +64,20 @@ class ProcedureCreation(QDialog):
def set_html(self):
from .equipment_usage_2 import EquipmentUsage
logger.debug(f"Edit: {self.edit}")
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']))
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)))
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(
template_name="procedure_creation",
@@ -81,13 +86,20 @@ class ProcedureCreation(QDialog):
proceduretype=proceduretype_dict,
run=self.run.details_dict(),
procedure=self.procedure.__dict__,
plate_map=self.plate_map
plate_map=self.plate_map,
edit=self.edit
)
with open("web.html", "w") as f:
f.write(html)
self.webview.setHtml(html)
@pyqtSlot(str, str)
def text_changed(self, key: str, new_value: str):
logger.debug(f"New value for {key}: {new_value}")
match key:
case "rsl_plate_num":
setattr(self.procedure.run, key, new_value)
case _:
attribute = getattr(self.procedure, key)
attribute['value'] = new_value.strip('\"')
@@ -113,7 +125,6 @@ class ProcedureCreation(QDialog):
def return_sql(self):
return self.procedure.to_sql()
# class ProcedureWebViewer(QWebEngineView):
#
# def __init__(self, *args, **kwargs):

View File

@@ -0,0 +1,119 @@
.flexDiv {
display: flex;
flex-direction: column;
align-items: flex-start;
width: fit-content;
margin: 32px;
}
.selectWrapper {
display: none;
width: 100%;
position: absolute;
pointer-events: none;
transition: opacity 100ms linear 0s;
filter: drop-shadow(0 6px 26px rgba(0, 0, 0, 0.24));
padding-top: calc(var(--sizeVar) / 2);
}
.multiSelect {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
border: 1px solid var(--borderColor);
box-sizing: border-box;
border-radius: calc(var(--sizeVar) / 2);
position: absolute;
width: auto;
left: 0;
right: 0;
overflow: hidden;
background: #ffffff;
transition: transform 300ms ease-in-out 0s, clip-path 300ms ease-in-out 0s;
}
.multiSelect div {
color: var(--textPrimary);
padding: 16px;
width: auto;
cursor: pointer;
}
.multiSelect div:hover {
background-color: #f6f6f6;
}
.bottomBorder {
border-bottom: 1px solid var(--borderColor);
}
.topBorder {
border-top: 1px solid var(--borderColor);
}
.iconDiv {
display: flex;
align-items: center;
justify-content: space-between;
}
.noSpace {
justify-content: flex-start;
gap: 6px;
}
.titleDiv {
pointer-events: none;
font-weight: 700;
}
.justHover i {
opacity: 0;
}
.justHover:hover i {
opacity: 1;
}
.multiSelect .placeholder {
color: var(--textSecondary);
font-style: italic;
}
.multiSelect .narrow {
padding-top: 10px;
padding-bottom: 10px;
}
.multiSelect i {
color: var(--textSecondary);
}
.multiSelect {
transform: translateX(100%);
clip-path: polygon(0 0, 0 0, 0 100%, 0% 100%);
}
.multiSelect:nth-of-type(1) {
transform: translateX(0);
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
}
.sec_btn {
--bgColor: #869cff;
}
button {
font-family: "Roboto", sans-serif;
font-size: calc(var(--sizeVar) * 1.75);
font-weight: 500;
border: none;
outline: none;
padding: var(--sizeVar) calc(var(--sizeVar) * 2);
border-radius: calc(var(--sizeVar) / 2);
cursor: pointer;
background-color: var(--bgColor);
color: var(--txtColor);
box-shadow: 0 0 0 1px var(--borColor) inset;
}
button:focus {
--borColor: rgba(0, 0, 0, 0.4);
}
button:hover {
--bgColor: #1fcc9e;
}
.sec_btn:hover {
--bgColor: #6279e7;
}
.tri_btn:hover {
--bgColor: #f8f7f8;
}
button:active {
--bgColor: #1db284;
}
.sec_btn:active {
--bgColor: #5468c7;
}
.tri_btn:active {
--bgColor: #e7e7e7;
}

View File

@@ -76,7 +76,7 @@ div.procedure_creation_leftright div.right {
width: 100%;
}
.form_text {
.form_text.full {
width: 97%;
}

View File

@@ -0,0 +1,47 @@
function openMulti() {
if (document.querySelector(".selectWrapper").style.pointerEvents == "all") {
document.querySelector(".selectWrapper").style.opacity = 0;
document.querySelector(".selectWrapper").style.pointerEvents = "none";
resetAllMenus();
} else {
document.querySelector(".selectWrapper").style.opacity = 1;
document.querySelector(".selectWrapper").style.pointerEvents = "all";
}
}
function nextMenu(e) {
menuIndex = eval(event.target.parentNode.id.slice(-1));
document.querySelectorAll(".multiSelect")[menuIndex].style.transform =
"translateX(-100%)";
// document.querySelectorAll(".multiSelect")[menuIndex].style.clipPath = "polygon(0 0, 0 0, 0 100%, 0% 100%)";
document.querySelectorAll(".multiSelect")[menuIndex].style.clipPath =
"polygon(100% 0, 100% 0, 100% 100%, 100% 100%)";
document.querySelectorAll(".multiSelect")[menuIndex + 1].style.transform =
"translateX(0)";
document.querySelectorAll(".multiSelect")[menuIndex + 1].style.clipPath =
"polygon(0 0, 100% 0, 100% 100%, 0% 100%)";
}
function prevMenu(e) {
menuIndex = eval(event.target.parentNode.id.slice(-1));
document.querySelectorAll(".multiSelect")[menuIndex].style.transform =
"translateX(100%)";
document.querySelectorAll(".multiSelect")[menuIndex].style.clipPath =
"polygon(0 0, 0 0, 0 100%, 0% 100%)";
document.querySelectorAll(".multiSelect")[menuIndex - 1].style.transform =
"translateX(0)";
document.querySelectorAll(".multiSelect")[menuIndex - 1].style.clipPath =
"polygon(0 0, 100% 0, 100% 100%, 0% 100%)";
}
function resetAllMenus() {
setTimeout(function () {
var x = document.getElementsByClassName("multiSelect");
var i;
for (i = 1; i < x.length; i++) {
x[i].style.transform = "translateX(100%)";
x[i].style.clipPath = "polygon(0 0, 0 0, 0 100%, 0% 100%)";
}
document.querySelectorAll(".multiSelect")[0].style.transform =
"translateX(0)";
document.querySelectorAll(".multiSelect")[0].style.clipPath =
"polygon(0 0, 100% 0, 100% 100%, 0% 100%)";
}, 300);
}

View File

@@ -1,3 +1,6 @@
document.getElementById("kittype").addEventListener("change", function() {
backend.update_kit(this.value);
})
@@ -17,3 +20,71 @@ for(let i = 0; i < formtexts.length; i++) {
backend.text_changed(formtexts[i].id, formtexts[i].value);
})
};
var reagentRoles = document.getElementsByClassName("reagentrole");
for(let i = 0; i < reagentRoles.length; i++) {
reagentRoles[i].addEventListener("change", function() {
if (reagentRoles[i].value === "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");
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);
new_form.appendChild(br.cloneNode());
new_form.appendChild(rr_lot_label);
new_form.appendChild(rr_lot);
new_form.appendChild(br.cloneNode());
new_form.appendChild(rr_expiry_label);
new_form.appendChild(rr_expiry);
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);
}
new_reg.appendChild(new_form);
}
})
};

View File

@@ -4,18 +4,24 @@
<head>
{% block head %}
{{ super() }}
<title> New {{ proceduretype['name'] }} for {{ run['plate_number'] }}</title>
<title>{% if edit %}Edit{% else %}New{% endif %} {{ proceduretype['name'] }} for {{ run['plate_number'] }}</title>
{% endblock %}
</head>
<!-- Is this working? -->
<body>
{% block body %}
<h1>New {{ proceduretype['name'] }} for {{ run['plate_number'] }}</h1><br><br>
<h1>{% if edit %}Edit{% else %}New{% endif %} {{ proceduretype['name'] }} for
{% if run['plate_number'] %}
<a style="height:30px; font-size:30px" id="rsl_plate_num">{{ run['plate_number'] }}</a>
{% else %}
<input style="height:30px; font-size:30px" type="text" id="rsl_plate_num" value="RSL-XX-YYYYMMDD-P">
{% endif %}
</h1><br><br>
<div class="procedure_creation_leftright">
<div class="left">
<form>
<label for="technician">Technician:</label><br>
<input type="text" class="form_text" id="technician" name="technician" width="100%" value="{{ procedure['technician']['value'] }}" background-color="{{ procedure['technician']['colour'] }}"><br><br>
<input type="text" class="form_text full" id="technician" name="technician" width="100%" value="{{ procedure['technician']['value'] }}" background-color="{{ procedure['technician']['colour'] }}"><br><br>
<label for="repeat">Repeat:</label>
<input type="checkbox" class="form_check" id="repeat" name="repeat" value="{{ procedure['repeat'] }}"><br><br>
<label>Kit Type:</label><br>
@@ -33,6 +39,7 @@
<option value="{{ reagent }}">{{ reagent }}</option>
{% endfor %}
</select>
<div class="new_reagent" id="new_{{ key }}"></div>
{% endfor %}
{% endif %}
</form>
@@ -44,9 +51,9 @@
{% endif %}
</div>
</div>
{% with proceduretype=proceduretype, child=True %}
{% include "support/equipment_usage.html" %}
{% endwith %}
{% if proceduretype['equipment_section'] %}
{{ proceduretype['equipment_section'] }}
{% endif %}
{% include 'support/context_menu.html' %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
<nav class="context-menu" id="context-menu">
<!-- <button class="sec_btn" onclick="openMulti();">Add to feature vector</button>-->
<div class="selectWrapper">
<div class="multiSelect context-menu__items" id="menu-0">
<ul class="bottomBorder" id="menu-header"></ul>
<div class="context-menu__item">
<a href="" class="context-menu__link" data-action="insertSample">Insert Sample</a>
</div>
<div class="context-menu__link">
<div onclick="openMulti();">myVector</div>
</div>
<div class="context-menu__link">
<div onclick="openMulti();">featureVector</div>
</div>
</div>
</div>
<!-- <div class="multiSelect" id="menu-1">-->
<!-- <div class="bottomBorder iconDiv noSpace narrow placeholder"><i class="material-icons">search</i>Search</div>-->
<!-- <div class="iconDiv justHover" onclick="nextMenu(event);">Project Example<i class="material-icons">arrow_right</i></div>-->
<!-- <div class="iconDiv justHover" onclick="nextMenu(event);">Davids project<i class="material-icons">arrow_right</i></div>-->
<!-- <div class="iconDiv justHover" onclick="nextMenu(event);">Project Idan<i class="material-icons">arrow_right</i></div>-->
<!-- <div class="iconDiv justHover" onclick="nextMenu(event);">Manhattan<i class="material-icons">arrow_right</i></div>-->
<!-- <div class="topBorder iconDiv noSpace" onclick="prevMenu(event);"><i class="material-icons">arrow_back</i>Back</div>-->
<!-- </div>-->
<!-- <div class="multiSelect" id="menu-2">-->
<!-- <div class="bottomBorder titleDiv">Project Idan</div>-->
<!-- <div onclick="openMulti();">Idan Vector</div>-->
<!-- <div onclick="openMulti();">Testings</div>-->
<!-- <div onclick="openMulti();">Features_120</div>-->
<!-- <div onclick="openMulti();">Aggregators</div>-->
<!-- <div id="menu-1" class="topBorder iconDiv noSpace" onclick="prevMenu(event);"><i class="material-icons">arrow_back</i>Back</div>-->
<!-- </div>-->
</nav>

View File

@@ -29,7 +29,7 @@ from functools import wraps
timezone = tz("America/Winnipeg")
logger = logging.getLogger(f"procedure.{__name__}")
logger = logging.getLogger(f"submissions.{__name__}")
logger.info(f"Package dir: {project_path}")
@@ -463,6 +463,7 @@ def render_details_template(template_name:str, css_in:List[str]|str=[], js_in:Li
for js in js_in:
with open(js, "r") as f:
js_out.append(f.read())
logger.debug(f"Kwargs: {kwargs}")
return template.render(css=css_out, js=js_out, **kwargs)