Start of custom context menu for procedure creation

This commit is contained in:
lwark
2025-05-29 14:13:16 -05:00
parent fef964fba0
commit a2e1c52f22
6 changed files with 179 additions and 58 deletions

View File

@@ -1297,7 +1297,7 @@ class Run(BaseClass, LogMixin):
submission_rank = self.get_submission_rank_of_sample(sample=sample) submission_rank = self.get_submission_rank_of_sample(sample=sample)
if submission_rank != 0: if submission_rank != 0:
row, column = plate_dict[submission_rank] 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: else:
unranked_samples.append(sample) 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]) 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 continue
row, column = plate_dict[submission_rank] row, column = plate_dict[submission_rank]
ranked_samples.append( 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")) background_color="#6ffe1d"))
padded_list = [] padded_list = []
for iii in range(1, proceduretype.total_wells+1): for iii in range(1, proceduretype.total_wells+1):
sample = next((item for item in ranked_samples if item['submission_rank']==iii), 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) 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'))) return list(sorted(padded_list, key=itemgetter('submission_rank')))
class SampleType(BaseClass): class SampleType(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), nullable=False, unique=True) #: identification from submitter name = Column(String(64), nullable=False, unique=True) #: identification from submitter

View File

@@ -1397,53 +1397,17 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
self.reagentrole = {} self.reagentrole = {}
self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype))) 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): def update_samples(self, sample_list: List[dict]):
logger.debug(f"Attempting sample shuffle.") logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}")
try: for sample_dict in sample_list:
source_sample = next( try:
(sample for sample in self.samples if sample.row == source_row and sample.column == source_column)) sample = next((item for item in self.samples if item.sample_id.upper()==sample_dict['sample_id'].upper()))
except StopIteration: except StopIteration:
raise StopIteration("Couldn't find proper sample.") continue
logger.debug(f"Source Well: {source_row}, {source_column}") row, column = self.proceduretype.ranked_plate[sample_dict['index']]
logger.debug(f"Destination Well: {destination_row}, {destination_column}") sample.row = row
updateable_samples = [] sample.column = column
if source_row > destination_row and source_column >= destination_column: logger.debug(f"Updated samples:\n{pformat(self.samples)}")
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
class PydClientSubmission(PydBaseClass): class PydClientSubmission(PydBaseClass):

View File

@@ -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())

View File

@@ -109,4 +109,14 @@ div.gallery {
.grid-item p { .grid-item p {
max-width: 100%; max-width: 100%;
border-radius: 8px; border-radius: 8px;
} }
.context-menu {
display: none;
position: absolute;
z-index: 10;
}
.context-menu--active {
display: block;
}

View File

@@ -1,6 +1,6 @@
<div class="plate" id="plate-container" style="grid-template-columns: repeat({{ plate_columns }}, {{ vw }}vw);grid-template-rows: repeat({{ plate_rows }}, {{ vw }}vw);"> <div class="plate" id="plate-container" style="grid-template-columns: repeat({{ plate_columns }}, {{ vw }}vw);grid-template-rows: repeat({{ plate_rows }}, {{ vw }}vw);">
{% for sample in samples %} {% for sample in samples %}
<div class="well" draggable="true" id="sample_{{ sample['submission_rank'] }}" style="background-color: {{ sample['background_color'] }};"> <div class="well" draggable="true" id="{{ sample['well_id'] }}" style="background-color: {{ sample['background_color'] }};">
<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p> <p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p>
<!-- <div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}--> <!-- <div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}-->
<!-- <span class="tooltiptext">{{ sample['tooltip'] }}</span>--> <!-- <span class="tooltiptext">{{ sample['tooltip'] }}</span>-->

View File

@@ -45,6 +45,25 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
<nav class="context-menu">
<ul class="context-menu__items">
<li class="context-menu__item">
<a href="#" class="context-menu__link">
<i class="fa fa-eye"></i> View Task
</a>
</li>
<li class="context-menu__item">
<a href="#" class="context-menu__link">
<i class="fa fa-edit"></i> Edit Task
</a>
</li>
<li class="context-menu__item">
<a href="#" class="context-menu__link">
<i class="fa fa-times"></i> Delete Task
</a>
</li>
</ul>
</nav>
</body> </body>
<script> <script>
{% block script %} {% block script %}
@@ -114,11 +133,25 @@
output = []; output = [];
fullGrid = [...gridContainer.children]; fullGrid = [...gridContainer.children];
fullGrid.forEach(function(item, index) { fullGrid.forEach(function(item, index) {
output.push({item: item, index: index}) output.push({sample_id: item.id, index: index + 1})
}); });
backend.replow(output); backend.rearrange_plate(output);
} }
}); });
// Context Menu Functionality
var wells = document.querySelectorAll(".well");
for ( var i = 0, len = wells.length; i < len; i++ ) {
var welloi = wells[i];
welloi.addEventListener( "contextmenu", function(e) {
well = e.target
backend.log_drag(well.id, "el");
});
};
{% endblock %} {% endblock %}
</script> </script>
</html> </html>