diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 11093b2..edeba6d 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -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") diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index ddfd9be..6ef9037 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -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))) diff --git a/src/submissions/frontend/widgets/equipment_usage_2.py b/src/submissions/frontend/widgets/equipment_usage_2.py new file mode 100644 index 0000000..42974fc --- /dev/null +++ b/src/submissions/frontend/widgets/equipment_usage_2.py @@ -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() diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index dcfa601..bc33805 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -14,23 +14,24 @@ 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 - self.setWindowTitle(f"New {self.proceduretype.name} for { self.run.rsl_plate_number }") + self.setWindowTitle(f"New {self.proceduretype.name} for {self.run.rsl_plate_number}") # self.created_procedure = self.proceduretype.construct_dummy_procedure(run=self.run) 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) @@ -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,15 +86,22 @@ 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}") - attribute = getattr(self.procedure, key) - attribute['value'] = new_value.strip('\"') + match key: + case "rsl_plate_num": + setattr(self.procedure.run, key, new_value) + case _: + attribute = getattr(self.procedure, key) + attribute['value'] = new_value.strip('\"') @pyqtSlot(str, bool) def check_toggle(self, key: str, ischecked: bool): @@ -113,16 +125,15 @@ class ProcedureCreation(QDialog): def return_sql(self): return self.procedure.to_sql() - # class ProcedureWebViewer(QWebEngineView): # # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # # def contextMenuEvent(self, event: QContextMenuEvent): - # self.menu = self.page().createStandardContextMenu() - # self.menu = self.createStandardContextMenu() - # add_sample = QAction("Add Sample") - # self.menu = QMenu() - # self.menu.addAction(add_sample) - # self.menu.popup(event.globalPos()) +# self.menu = self.page().createStandardContextMenu() +# self.menu = self.createStandardContextMenu() +# add_sample = QAction("Add Sample") +# self.menu = QMenu() +# self.menu.addAction(add_sample) +# self.menu.popup(event.globalPos()) diff --git a/src/submissions/templates/css/new_context_menu.css b/src/submissions/templates/css/new_context_menu.css new file mode 100644 index 0000000..5caac50 --- /dev/null +++ b/src/submissions/templates/css/new_context_menu.css @@ -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; +} diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css index 9885374..731865c 100644 --- a/src/submissions/templates/css/styles.css +++ b/src/submissions/templates/css/styles.css @@ -76,7 +76,7 @@ div.procedure_creation_leftright div.right { width: 100%; } -.form_text { +.form_text.full { width: 97%; } diff --git a/src/submissions/templates/js/new_context_menu.js b/src/submissions/templates/js/new_context_menu.js new file mode 100644 index 0000000..1c37f1c --- /dev/null +++ b/src/submissions/templates/js/new_context_menu.js @@ -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); +} diff --git a/src/submissions/templates/js/procedure_form.js b/src/submissions/templates/js/procedure_form.js index 825ac15..feff608 100644 --- a/src/submissions/templates/js/procedure_form.js +++ b/src/submissions/templates/js/procedure_form.js @@ -1,3 +1,6 @@ + + + document.getElementById("kittype").addEventListener("change", function() { backend.update_kit(this.value); }) @@ -16,4 +19,72 @@ for(let i = 0; i < formtexts.length; i++) { formtexts[i].addEventListener("input", function() { backend.text_changed(formtexts[i].id, formtexts[i].value); }) -}; \ No newline at end of file +}; + +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); + } + }) +}; + diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index 07c5911..ef4a65c 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -4,18 +4,24 @@
{% block head %} {{ super() }} -