From a2e1c52f22aad34297591c530b2b796258fa3010 Mon Sep 17 00:00:00 2001 From: lwark Date: Thu, 29 May 2025 14:13:16 -0500 Subject: [PATCH] Start of custom context menu for procedure creation --- .../backend/db/models/submissions.py | 11 +- src/submissions/backend/validators/pydant.py | 58 ++------- .../frontend/widgets/procedure_creation.py | 117 ++++++++++++++++++ src/submissions/templates/css/styles.css | 12 +- src/submissions/templates/plate_map.html | 2 +- .../templates/procedure_creation.html | 37 +++++- 6 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 src/submissions/frontend/widgets/procedure_creation.py diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 2705762..56c2396 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1297,7 +1297,7 @@ class Run(BaseClass, LogMixin): submission_rank = self.get_submission_rank_of_sample(sample=sample) if submission_rank != 0: row, column = plate_dict[submission_rank] - ranked_samples.append(dict(sample_id=sample.sample_id, row=row, column=column, submission_rank=submission_rank, background_color="#6ffe1d")) + ranked_samples.append(dict(well_id=sample.sample_id, sample_id=sample.sample_id, row=row, column=column, submission_rank=submission_rank, background_color="#6ffe1d")) else: unranked_samples.append(sample) possible_ranks = (item for item in list(plate_dict.keys()) if item not in [sample['submission_rank'] for sample in ranked_samples]) @@ -1310,21 +1310,18 @@ class Run(BaseClass, LogMixin): continue row, column = plate_dict[submission_rank] ranked_samples.append( - dict(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")) padded_list = [] for iii in range(1, proceduretype.total_wells+1): sample = next((item for item in ranked_samples if item['submission_rank']==iii), - dict(sample_id="", row=0, column=0, submission_rank=iii) + dict(well_id=f"blank_{iii}", sample_id="", row=0, column=0, submission_rank=iii, background_color="#ffffff") ) 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'))))}") return list(sorted(padded_list, key=itemgetter('submission_rank'))) - - - class SampleType(BaseClass): id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(64), nullable=False, unique=True) #: identification from submitter diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 36c3dd9..e6801e8 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -1397,53 +1397,17 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): self.reagentrole = {} self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype))) - def shuffle_samples(self, source_row: int, source_column: int, destination_row: int, destination_column=int): - logger.debug(f"Attempting sample shuffle.") - try: - source_sample = next( - (sample for sample in self.samples if sample.row == source_row and sample.column == source_column)) - except StopIteration: - raise StopIteration("Couldn't find proper sample.") - logger.debug(f"Source Well: {source_row}, {source_column}") - logger.debug(f"Destination Well: {destination_row}, {destination_column}") - updateable_samples = [] - if source_row > destination_row and source_column >= destination_column: - logger.debug(f"Sample was moved ahead.") - movement = "pos" - for sample in self.samples: - if sample.row >= destination_row and sample.column >= destination_column: - if sample.row <= source_row and sample.column <= source_column: - updateable_samples.append(sample) - - elif source_row < destination_row and source_column <= destination_column: - logger.debug(f"Sample was moved back.") - movement = "neg" - for sample in self.samples: - if sample.row <= destination_row and sample.column <= destination_column: - if sample.row >= source_row and sample.column >= source_column: - updateable_samples.append(sample) - else: - logger.debug(f"Don't know what happened.") - logger.debug(f"Samples to be updated: {pformat(updateable_samples)}") - for sample in updateable_samples: - if sample.row == source_row and sample.column == source_column: - sample.row = destination_row - sample.column = destination_column - else: - match movement: - case "pos": - if sample.row + 1 > 8: - sample.column += 1 - sample.row = 1 - else: - sample.row += 1 - case "neg": - if sample.row - 1 <= 0: - sample.column -= 1 - sample.row = 8 - else: - sample.row -= 1 - + def update_samples(self, sample_list: List[dict]): + logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}") + for sample_dict in sample_list: + try: + sample = next((item for item in self.samples if item.sample_id.upper()==sample_dict['sample_id'].upper())) + except StopIteration: + continue + row, column = self.proceduretype.ranked_plate[sample_dict['index']] + sample.row = row + sample.column = column + logger.debug(f"Updated samples:\n{pformat(self.samples)}") class PydClientSubmission(PydBaseClass): diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py new file mode 100644 index 0000000..611cdcb --- /dev/null +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -0,0 +1,117 @@ +""" + +""" +from __future__ import annotations +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 +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from backend.db.models import Run, ProcedureType +from tools import jinja_template_loading, get_application_from_parent +from backend.validators import PydProcedure + +logger = logging.getLogger(f"submissions.{__name__}") + + +class ProcedureCreation(QDialog): + + def __init__(self, parent, run: Run, proceduretype: ProcedureType): + super().__init__(parent) + self.run = run + self.proceduretype = proceduretype + self.setWindowTitle(f"New {proceduretype.name} for { run.rsl_plate_num }") + self.created_procedure = self.proceduretype.construct_dummy_procedure(run=self.run) + self.created_procedure.update_kittype_reagentroles(kittype=self.created_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.created_procedure.samples) + # 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) + + def set_html(self): + env = jinja_template_loading() + template = env.get_template("procedure_creation.html") + template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) + with open(template_path.joinpath("css", "styles.css"), "r") as f: + css = f.read() + html = template.render(proceduretype=self.proceduretype.as_dict, run=self.run.to_dict(), + procedure=self.created_procedure.__dict__, plate_map=self.plate_map, css=css) + with open("procedure.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.created_procedure, key) + attribute['value'] = new_value + + @pyqtSlot(str, bool) + def check_toggle(self, key: str, ischecked: bool): + # logger.debug(f"{key} is checked: {ischecked}") + setattr(self.created_procedure, key, ischecked) + + @pyqtSlot(str) + def update_kit(self, kittype): + self.created_procedure.update_kittype_reagentroles(kittype=kittype) + logger.debug({k: v for k, v in self.created_procedure.__dict__.items() if k != "plate_map"}) + self.set_html() + + @pyqtSlot(list) + def rearrange_plate(self, sample_list: list): + self.created_procedure.update_samples(sample_list=sample_list) + + @pyqtSlot(str, str) + def log_drag(self, source_well: str, destination_well: str): + logger.debug(f"Source Index: {source_well} Destination Index: {destination_well}") + # source_well = source_well.split("-") + # destination_well = destination_well.split("-") + # source_row = int(source_well[0]) + # source_column = int(source_well[1]) + # destination_row = int(destination_well[0]) + # destination_column = int(destination_well[1]) + # self.created_procedure.shuffle_samples( + # source_row=source_row, + # source_column=source_column, + # destination_row=destination_row, + # destination_column=destination_column + # ) + + +# 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()) diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css index 45488b9..5ac5add 100644 --- a/src/submissions/templates/css/styles.css +++ b/src/submissions/templates/css/styles.css @@ -109,4 +109,14 @@ div.gallery { .grid-item p { max-width: 100%; border-radius: 8px; -} \ No newline at end of file +} + +.context-menu { + display: none; + position: absolute; + z-index: 10; +} + +.context-menu--active { + display: block; +} diff --git a/src/submissions/templates/plate_map.html b/src/submissions/templates/plate_map.html index d7aa2bb..70ca8e5 100644 --- a/src/submissions/templates/plate_map.html +++ b/src/submissions/templates/plate_map.html @@ -1,6 +1,6 @@
{% for sample in samples %} -
+

{{ sample['sample_id'] }}

diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index 23d72a3..0f6fb1c 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -45,6 +45,25 @@
{% endblock %} + \ No newline at end of file