diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index c49beb0..5156f18 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1207,7 +1207,7 @@ class ProcedureType(BaseClass): # NOTE: An overly complicated list comprehension create a list of sample locations # NOTE: next will return a blank cell if no value found for row/column env = jinja_template_loading() - template = env.get_template("plate_map.html") + template = env.get_template("support/plate_map.html") html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=sample_dicts, vw=vw) return html + "
" diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 56c2396..c83dc17 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -682,7 +682,7 @@ class Run(BaseClass, LogMixin): for row in rows for column in columns] env = jinja_template_loading() - template = env.get_template("plate_map.html") + template = env.get_template("support/plate_map.html") html = template.render(samples=output_samples, PLATE_ROWS=plate_rows, PLATE_COLUMNS=plate_columns) return html + "
" @@ -1626,7 +1626,7 @@ class ClientSubmissionSampleAssociation(BaseClass): # NOTE: Since there is no PCR, negliable result is necessary. sample = self.to_sub_dict() env = jinja_template_loading() - template = env.get_template("tooltip.html") + template = env.get_template("support/tooltip.html") tooltip_text = template.render(fields=sample) try: control = self.sample.control @@ -1880,7 +1880,7 @@ class RunSampleAssociation(BaseClass): # NOTE: Since there is no PCR, negliable result is necessary. sample = self.to_sub_dict() env = jinja_template_loading() - template = env.get_template("tooltip.html") + template = env.get_template("support/tooltip.html") tooltip_text = template.render(fields=sample) try: control = self.sample.control diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index 611cdcb..aabf1d2 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -2,6 +2,8 @@ """ from __future__ import annotations + +import os import sys, logging from pathlib import Path from pprint import pformat @@ -15,7 +17,7 @@ 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 tools import jinja_template_loading, get_application_from_parent, render_details_template from backend.validators import PydProcedure logger = logging.getLogger(f"submissions.{__name__}") @@ -54,13 +56,15 @@ class ProcedureCreation(QDialog): 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) + html = render_details_template( + template_name="procedure_creation", + # css_in=['new_context_menu'], + js_in=["procedure_form", "grid_drag", "context_menu"], + proceduretype=self.proceduretype.as_dict, + run=self.run.to_dict(), + procedure=self.created_procedure.__dict__, + plate_map=self.plate_map + ) with open("procedure.html", "w") as f: f.write(html) self.webview.setHtml(html) @@ -86,21 +90,9 @@ class ProcedureCreation(QDialog): 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 - # ) + @pyqtSlot(str) + def log(self, logtext: str): + logger.debug(logtext) # class ProcedureWebViewer(QWebEngineView): diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css index 5ac5add..11df3b1 100644 --- a/src/submissions/templates/css/styles.css +++ b/src/submissions/templates/css/styles.css @@ -115,6 +115,9 @@ div.gallery { display: none; position: absolute; z-index: 10; + background-color: rgba(229, 231, 228, 0.7); + border-radius: 2px; + border-color: black; } .context-menu--active { diff --git a/src/submissions/templates/details.html b/src/submissions/templates/details.html index c98bb69..43e4ba5 100644 --- a/src/submissions/templates/details.html +++ b/src/submissions/templates/details.html @@ -5,7 +5,10 @@ {% if css %} {% endif %} @@ -18,12 +21,12 @@ {% endblock %} {% block signing_button %}{% endblock %} - +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/src/submissions/templates/js/context_menu.js b/src/submissions/templates/js/context_menu.js new file mode 100644 index 0000000..65a221c --- /dev/null +++ b/src/submissions/templates/js/context_menu.js @@ -0,0 +1,260 @@ +//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); +//} + + +/////////////////////////////////////// +/////////////////////////////////////// +// +// H E L P E R F U N C T I O N S +// +/////////////////////////////////////// +/////////////////////////////////////// + +function clickInsideElement( e, className ) { + var el = e.srcElement || e.target; + if ( el.classList.contains(className) ) { + return el; + } else { + while ( el = el.parentNode ) { + if ( el.classList && el.classList.contains(className) ) { + return el; + } + } + } + return false; +} + +function getPosition(e) { + var posx = 0; + var posy = 0; + if (!e) var e = window.event; + if (e.pageX || e.pageY) { + posx = e.pageX; + posy = e.pageY; + } else if (e.clientX || e.clientY) { + posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + return { + x: posx, + y: posy + } +} + +// updated positionMenu function +function positionMenu(e) { + clickCoords = getPosition(e); + clickCoordsX = clickCoords.x; + clickCoordsY = clickCoords.y; + menuWidth = menu.offsetWidth + 4; + menuHeight = menu.offsetHeight + 4; + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + if ( (windowWidth - clickCoordsX) < menuWidth ) { + menu.style.left = windowWidth - menuWidth + "px"; + } else { + menu.style.left = clickCoordsX + "px"; + } + if ( (windowHeight - clickCoordsY) < menuHeight ) { + menu.style.top = windowHeight - menuHeight + "px"; + } else { + menu.style.top = clickCoordsY + "px"; + } +} + +function menuItemListener( link ) { + const contextIndex = [...gridContainer.children].indexOf(taskItemInContext); + const task_id = taskItemInContext.getAttribute("id") + backend.log("Task action - " + link.getAttribute("data-action")) + switch (link.getAttribute("data-action")) { + case "InsertSample": + insertSample(contextIndex, task_id); + break; + case "InsertControl": + insertControl(contextIndex); + break; + case "RemoveSample": + removeSample(contextIndex); + break; + default: + backend.log("default"); + break; + } + toggleMenuOff(); +} + +/////////////////////////////////////// +/////////////////////////////////////// +// +// C O R E F U N C T I O N S +// +/////////////////////////////////////// +/////////////////////////////////////// +/** +* Variables. +*/ +var contextMenuClassName = "context-menu"; +var contextMenuItemClassName = "context-menu__item"; +var contextMenuLinkClassName = "context-menu__link"; +var contextMenuActive = "context-menu--active"; +var taskItemClassName = "well"; +var taskItemInContext; +var clickCoords; +var clickCoordsX; +var clickCoordsY; +var menu = document.getElementById(contextMenuClassName); +var menuItems = menu.getElementsByClassName(contextMenuItemClassName); +var menuHeader = document.getElementById("menu-header"); +var menuState = 0; +var menuWidth; +var menuHeight; +var menuPosition; +var menuPositionX; +var menuPositionY; +var windowWidth; +var windowHeight; +/** +* Initialise our application's code. +*/ +function init() { + contextListener(); + clickListener(); + keyupListener(); + resizeListener(); +} +/** +* Listens for contextmenu events. +*/ +function contextListener() { + document.addEventListener( "contextmenu", function(e) { + taskItemInContext = clickInsideElement( e, taskItemClassName ); + if ( taskItemInContext ) { + e.preventDefault(); + menuHeader.innerText = taskItemInContext.id; + toggleMenuOn(); + positionMenu(e); + } else { + taskItemInContext = null; + menuHeader.text = ""; + toggleMenuOff(); + } + }); +} + +/** +* Listens for click events. +*/ +function clickListener() { + document.addEventListener( "click", function(e) { + var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName ); + if ( clickeElIsLink ) { + e.preventDefault(); + menuItemListener( clickeElIsLink ); + } else { + var button = e.which || e.button; + if ( button === 1 ) { + toggleMenuOff(); + } + } + }); +} +/** +* Listens for keyup events. +*/ +function keyupListener() { + window.onkeyup = function(e) { + if ( e.keyCode === 27 ) { + toggleMenuOff(); + } + } +} +/** +* Turns the custom context menu on. +*/ +function toggleMenuOn() { + if ( menuState !== 1 ) { + menuState = 1; + menu.classList.add(contextMenuActive); + } +} +function toggleMenuOff() { + if ( menuState !== 0 ) { + menuState = 0; + menu.classList.remove(contextMenuActive); + } +} + +/////////////////////////////////////// +/////////////////////////////////////// +// +// B A C K E N D F U N C T I O N S +// +/////////////////////////////////////// +/////////////////////////////////////// + +function insertSample( index ) { + backend.log( "Index - " + index + ", InsertSample"); +} + +function insertControl( index ) { + backend.log( "Index - " + index + ", InsertEN"); + var existing_ens = document.getElementsByClassName("EN"); + backend.log(existing_ens.length); +} + +function removeSample( index ) { + backend.log( "Index - " + index + ", RemoveSample"); +} + + +/** +* Run the app. +*/ +init(); + + diff --git a/src/submissions/templates/js/details.js b/src/submissions/templates/js/details.js new file mode 100644 index 0000000..e6b6de1 --- /dev/null +++ b/src/submissions/templates/js/details.js @@ -0,0 +1,4 @@ +var backend; +new QWebChannel(qt.webChannelTransport, function (channel) { + backend = channel.objects.backend; +}); \ No newline at end of file diff --git a/src/submissions/templates/js/grid_drag.js b/src/submissions/templates/js/grid_drag.js new file mode 100644 index 0000000..39f87c7 --- /dev/null +++ b/src/submissions/templates/js/grid_drag.js @@ -0,0 +1,51 @@ + + + const gridContainer = document.getElementById("plate-container"); + let draggedItem = null; + + //Handle Drag start + gridContainer.addEventListener("dragstart", (e) => { + draggedItem = e.target; + draggedItem.style.opacity = "0.5"; + }); + + //Handle Drag End + gridContainer.addEventListener("dragend", (e) => { + e.target.style.opacity = "1"; + draggedItem = null; + }); + + //handle dragging ove grid items + gridContainer.addEventListener("dragover", (e) => { + e.preventDefault(); + }); + + //Handle Drop + gridContainer.addEventListener("drop", (e) => { + e.preventDefault(); + + const targetItem = e.target; + if ( + targetItem && + targetItem !== draggedItem && + targetItem.classList.contains("well") + ) { + const draggedIndex = [...gridContainer.children].indexOf(draggedItem); + const targetIndex = [...gridContainer.children].indexOf(targetItem); + if (draggedIndex < targetIndex) { + backend.log_drag(draggedIndex.toString(), targetIndex.toString() + " Lesser"); + gridContainer.insertBefore(draggedItem, targetItem.nextSibling); + + } else { + backend.log_drag(draggedIndex.toString(), targetIndex.toString() + " Greater"); + gridContainer.insertBefore(draggedItem, targetItem); + + } + output = []; + fullGrid = [...gridContainer.children]; + fullGrid.forEach(function(item, index) { + output.push({sample_id: item.id, index: index + 1}) + }); + backend.rearrange_plate(output); + } + }); \ No newline at end of file diff --git a/src/submissions/templates/js/procedure_form.js b/src/submissions/templates/js/procedure_form.js new file mode 100644 index 0000000..825ac15 --- /dev/null +++ b/src/submissions/templates/js/procedure_form.js @@ -0,0 +1,19 @@ +document.getElementById("kittype").addEventListener("change", function() { + backend.update_kit(this.value); +}) + +var formchecks = document.getElementsByClassName('form_check'); + +for(let i = 0; i < formchecks.length; i++) { + formchecks[i].addEventListener("change", function() { + backend.check_toggle(formchecks[i].id, formchecks[i].checked); + }) +}; + +var formtexts = document.getElementsByClassName('form_text'); + +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 diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index 0f6fb1c..73370a9 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -44,114 +44,15 @@ {% endif %} + {% include 'support/context_menu.html' %} {% endblock %} - + - \ No newline at end of file diff --git a/src/submissions/templates/support/context_menu.html b/src/submissions/templates/support/context_menu.html new file mode 100644 index 0000000..ac4e38a --- /dev/null +++ b/src/submissions/templates/support/context_menu.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/src/submissions/templates/plate_map.html b/src/submissions/templates/support/plate_map.html similarity index 100% rename from src/submissions/templates/plate_map.html rename to src/submissions/templates/support/plate_map.html diff --git a/src/submissions/templates/tooltip.html b/src/submissions/templates/support/tooltip.html similarity index 100% rename from src/submissions/templates/tooltip.html rename to src/submissions/templates/support/tooltip.html diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 2fd88e8..cd00fc2 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -443,6 +443,30 @@ def jinja_template_loading() -> Environment: return env +def render_details_template(template_name:str, css_in:List[str]|str=[], js_in:List[str]|str=[], **kwargs) -> str: + if isinstance(css_in, str): + css_in = [css_in] + css_in = ["styles"] + css_in + css_in = [project_path.joinpath("src", "submissions", "templates", "css", f"{c}.css") for c in css_in] + if isinstance(js_in, str): + js_in = [js_in] + js_in = ["details"] + js_in + js_in = [project_path.joinpath("src", "submissions", "templates", "js", f"{j}.js") for j in js_in] + env = jinja_template_loading() + template = env.get_template(f"{template_name}.html") + # template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) + css_out = [] + for css in css_in: + with open(css, "r") as f: + css_out.append(f.read()) + js_out = [] + for js in js_in: + with open(js, "r") as f: + js_out.append(f.read()) + return template.render(css=css_out, js=js_out, **kwargs) + + + def convert_well_to_row_column(input_str: str) -> Tuple[int, int]: """ Converts typical alphanumeric (i.e. "A2") to row, column