Files
Submissions-App/src/submissions/frontend/widgets/procedure_creation.py
2025-08-13 09:45:51 -05:00

211 lines
9.4 KiB
Python

"""
"""
from __future__ import annotations
import datetime
import os
import re
import sys, logging
from pathlib import Path
from pprint import pformat
from PyQt6.QtCore import pyqtSlot, Qt
from PyQt6.QtGui import QContextMenuEvent, QAction
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QGridLayout, QMenu, QDialogButtonBox
from typing import TYPE_CHECKING, Any, List
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, sanitize_object_for_json
logger = logging.getLogger(f"submissions.{__name__}")
class ProcedureCreation(QDialog):
def __init__(self, parent, procedure: PydProcedure, edit: bool = False):
super().__init__(parent)
self.edit = edit
self.run = procedure.run
self.procedure = procedure
logger.debug(f"procedure: {pformat(self.procedure.__dict__)}")
self.proceduretype = procedure.proceduretype
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)
# logger.debug(f"Samples to map\n{pformat(self.created_procedure.samples)}")
self.plate_map = self.proceduretype.construct_plate_map(sample_dicts=self.procedure.sample)
self.procedure.update_samples(sample_list=[dict(sample_id=sample.sample_id, index=iii) for iii, sample in
enumerate(self.procedure.sample, start=1)])
# logger.debug(f"Plate map: {self.plate_map}")
# logger.debug(f"Created dummy: {self.created_procedure}")
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)
self.set_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)
def set_html(self):
from .equipment_usage_2 import EquipmentUsage
# logger.debug(f"Edit: {self.edit}")
proceduretype_dict = self.proceduretype.details_dict()
# logger.debug(f"Reagent roles: {self.procedure.reagentrole}")
# logger.debug(f"Equipment roles: {pformat(proceduretype_dict['equipment'])}")
# NOTE: Add --New-- as an option for reagents.
for key, value in self.procedure.reagentrole.items():
value.append(dict(name="--New--"))
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)
proceduretype_dict['equipment'] = [sanitize_object_for_json(object) for object in proceduretype_dict['equipment']]
self.update_equipment = EquipmentUsage.update_equipment
regex = re.compile(r".*R\d$")
proceduretype_dict['previous'] = [""] + [item.name for item in self.run.procedure if item.proceduretype == self.proceduretype and not bool(regex.match(item.name))]
html = render_details_template(
template_name="procedure_creation",
# css_in=['new_context_menu'],
js_in=["procedure_form", "grid_drag", "context_menu"],
proceduretype=proceduretype_dict,
run=self.run.details_dict(),
# procedure=self.procedure.__dict__,
procedure=self.procedure,
plate_map=self.plate_map,
edit=self.edit
)
with open("procedure_creation_rendered.html", "w") as f:
f.write(html)
self.webview.setHtml(html)
@pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
from backend.db.models import Equipment
logger.debug("Updating equipment")
try:
equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
except StopIteration:
equipment_of_interest = None
equipment = Equipment.query(name=equipment)
if equipment_of_interest:
eoi = self.procedure.equipment.pop(self.procedure.equipment.index(equipment_of_interest))
else:
eoi = equipment.to_pydantic(equipmentrole=equipmentrole, 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), None)
if process:
eoi.process = process.to_pydantic()
tips = next((tps for tps in equipment.tips if tps.name == tips), None)
if tips:
eoi.tips = tips.to_pydantic()
self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment: {self.procedure.equipment}")
@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)
match attribute:
case dict():
attribute['value'] = new_value.strip('\"')
case _:
setattr(self.procedure, key, new_value.strip('\"'))
logger.debug(f"Set value for {key}: {getattr(self.procedure, key)}")
@pyqtSlot(str, bool)
def check_toggle(self, key: str, ischecked: bool):
logger.debug(f"{key} is checked: {ischecked}")
setattr(self.procedure, key, ischecked)
@pyqtSlot(str)
def update_kit(self, kittype):
self.procedure.update_kittype_reagentroles(kittype=kittype)
logger.debug({k: v for k, v in self.procedure.__dict__.items() if k != "plate_map"})
self.set_html()
@pyqtSlot(list)
def rearrange_plate(self, sample_list: List[dict]):
self.procedure.update_samples(sample_list=sample_list)
@pyqtSlot(str)
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):
logger.debug(f"{reagentrole}: {name_lot_expiry}")
try:
name, lot, expiry = name_lot_expiry.split(" - ")
except ValueError as e:
logger.debug(f"Couldn't perform split due to {e}")
return
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
def return_sql(self, new: bool = False):
return self.procedure.to_sql(new=new)
# 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())