Attempt to update treeview.

This commit is contained in:
lwark
2025-06-04 07:18:21 -05:00
parent 26292e275c
commit db0b65b07b
11 changed files with 302 additions and 275 deletions

View File

@@ -22,7 +22,7 @@ from . import Base, BaseClass, ClientLab, LogMixin
from io import BytesIO
if TYPE_CHECKING:
from backend.db.models.submissions import Run
from backend.db.models.submissions import Run, ProcedureSampleAssociation
logger = logging.getLogger(f'procedure.{__name__}')
@@ -1175,8 +1175,9 @@ class ProcedureType(BaseClass):
proceduretype=self,
#name=dict(value=self.name, missing=True),
#possible_kits=[kittype.name for kittype in self.kittype],
repeat=False
repeat=False,
# plate_map=plate_map
run=run
)
return PydProcedure(**output)
@@ -1222,7 +1223,7 @@ class ProcedureType(BaseClass):
class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True)
name = Column(String, unique=True)
_name = Column(String, unique=True)
repeat = Column(INTEGER, nullable=False)
technician = Column(JSON) #: name of processing tech(s)
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL",
@@ -1236,13 +1237,23 @@ class Procedure(BaseClass):
kittype = relationship("KitType", back_populates="procedure")
control = relationship("Control", back_populates="procedure", uselist=True) #: A control sample added to procedure
proceduresampleassociation = relationship(
"ProcedureSampleAssociation",
back_populates="procedure",
cascade="all, delete-orphan",
)
sample = association_proxy("proceduresampleassociation",
"sample", creator=lambda sample: ProcedureSampleAssociation(sample=sample)
)
procedurereagentassociation = relationship(
"ProcedureReagentAssociation",
back_populates="procedure",
cascade="all, delete-orphan",
) #: Relation to ProcedureReagentAssociation
reagents = association_proxy("procedurereagentassociation",
reagent = association_proxy("procedurereagentassociation",
"reagent", creator=lambda reg: ProcedureReagentAssociation(
reagent=reg)) #: Association proxy to RunReagentAssociation.reagent
@@ -1263,6 +1274,14 @@ class Procedure(BaseClass):
tips = association_proxy("proceduretipsassociation",
"tips")
@hybrid_property
def name(self):
return f"{self.proceduretype.name}-{self.run.rsl_plate_num}"
@name.setter
def name(self, value):
self._name = value
@validates('repeat')
def validate_repeat(self, key, value):
if value > 1:
@@ -1290,6 +1309,12 @@ class Procedure(BaseClass):
pass
return cls.execute_query(query=query, limit=limit)
def to_dict(self, full_data: bool=False):
output = dict()
output['name'] = self.name
return output
class ProcedureTypeKitTypeAssociation(BaseClass):
"""
@@ -2608,171 +2633,4 @@ class ProcedureTipsAssociation(BaseClass):
from backend.validators import PydTips
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
#
# class ProcedureType(BaseClass):
# id = Column(INTEGER, primary_key=True)
# name = Column(String(64))
# reagent_map = Column(JSON)
#
# procedure = relationship("Procedure",
# back_populates="proceduretype") #: Concrete control of this type.
#
# process = relationship("Process", back_populates="proceduretype",
# secondary=proceduretype_process) #: Relation to equipment process used for this type.
#
# proceduretypekittypeassociation = relationship(
# "ProcedureTypeKitTypeAssociation",
# back_populates="proceduretype",
# cascade="all, delete-orphan",
# ) #: Association of kittypes
#
# kittype = association_proxy("proceduretypekittypeassociation", "kittype",
# creator=lambda kit: ProcedureTypeKitTypeAssociation(
# kittype=kit)) #: Proxy of kittype association
#
# proceduretypeequipmentroleassociation = relationship(
# "ProcedureTypeEquipmentRoleAssociation",
# back_populates="proceduretype",
# cascade="all, delete-orphan"
# ) #: Association of equipmentroles
#
# equipment = association_proxy("proceduretypeequipmentroleassociation", "equipmentrole",
# creator=lambda eq: ProcedureTypeEquipmentRoleAssociation(
# equipment_role=eq)) #: Proxy of equipmentrole associations
#
# kittypereagentroleassociation = relationship(
# "KitTypeReagentRoleAssociation",
# back_populates="proceduretype",
# cascade="all, delete-orphan"
# ) #: triple association of KitTypes, ReagentTypes, SubmissionTypes
#
# proceduretypetiproleassociation = relationship(
# "ProcedureTypeTipRoleAssociation",
# back_populates="proceduretype",
# cascade="all, delete-orphan"
# ) #: Association of tiproles
#
# def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]:
# """
# Make a map of all locations for tips or equipment.
#
# Args:
# field (Literal['equipment', 'tip']): the field to construct a map for
#
# Returns:
# Generator[(str, dict), None, None]: Generator composing key, locations for each item in the map
# """
# for item in self.__getattribute__(f"proceduretype{field}role_associations"):
# fmap = item.uses
# if fmap is None:
# fmap = {}
# yield getattr(item, f"{field}_role").name, fmap
#
# @property
# def default_kit(self) -> KitType | None:
# """
# If only one kits exists for this Submission Type, return it.
#
# Returns:
# KitType | None:
# """
# if len(self.kittype) == 1:
# return self.kittype[0]
# else:
# return None
#
# def get_equipment(self, kittype: str | KitType | None = None) -> Generator['PydEquipmentRole', None, None]:
# """
# Returns PydEquipmentRole of all equipment associated with this SubmissionType
#
# Returns:
# Generator['PydEquipmentRole', None, None]: List of equipment roles
# """
# return (item.to_pydantic(proceduretype=self, kittype=kittype) for item in self.equipment)
#
# def get_processes_for_role(self, equipmentrole: str | EquipmentRole, kittype: str | KitType | None = None) -> list:
# """
# Get process associated with this SubmissionType for an EquipmentRole
#
# Args:
# equipmentrole (str | EquipmentRole): EquipmentRole of interest
# kittype (str | KitType | None, optional): Kit of interest. Defaults to None.
#
# Raises:
# TypeError: Raised if wrong type given for equipmentrole
#
# Returns:
# list: list of associated process
# """
# match equipmentrole:
# case str():
# relevant = [item.get_all_processes(kittype) for item in self.proceduretypeequipmentroleassociation if
# item.equipmentrole.name == equipmentrole]
# case EquipmentRole():
# relevant = [item.get_all_processes(kittype) for item in self.proceduretypeequipmentroleassociation if
# item.equipmentrole == equipmentrole]
# case _:
# raise TypeError(f"Type {type(equipmentrole)} is not allowed")
# return list(set([item for items in relevant for item in items if item is not None]))
#
#
# class Procedure(BaseClass):
# id = Column(INTEGER, primary_key=True)
# name = Column(String, unique=True)
# technician = Column(JSON) #: name of processing tech(s)
# proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL",
# name="fk_PRO_proceduretype_id")) #: client lab id from _organizations))
# proceduretype = relationship("ProcedureType", back_populates="procedure")
# run_id = Column(INTEGER, ForeignKey("_run.id", ondelete="SET NULL",
# name="fk_PRO_basicrun_id")) #: client lab id from _organizations))
# run = relationship("Run", back_populates="procedure")
# kittype_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete="SET NULL",
# name="fk_PRO_kittype_id")) #: client lab id from _organizations))
# kittype = relationship("KitType", back_populates="procedure")
#
# control = relationship("Control", back_populates="procedure",
# uselist=True) #: A control sample added to procedure
#
# procedurereagentassociations = relationship(
# "ProcedureReagentAssociation",
# back_populates="procedure",
# cascade="all, delete-orphan",
# ) #: Relation to ProcedureReagentAssociation
#
# reagents = association_proxy("procedurereagentassociations",
# "reagent") #: Association proxy to RunReagentAssociation.reagent
#
# procedureequipmentassociations = relationship(
# "ProcedureEquipmentAssociation",
# back_populates="procedure",
# cascade="all, delete-orphan"
# ) #: Relation to Equipment
#
# equipment = association_proxy("procedureequipmentassociations",
# "equipment") #: Association proxy to RunEquipmentAssociation.equipment
#
# proceduretipsassociations = relationship(
# "ProcedureTipsAssociation",
# back_populates="procedure",
# cascade="all, delete-orphan")
#
# tips = association_proxy("proceduretipsassociations",
# "tips")
#
# @classmethod
# @setup_lookup
# def query(cls, id: int|None = None, name: str | None = None, limit: int = 0, **kwargs) -> Procedure | List[Procedure]:
# query: Query = cls.__database_session__.query(cls)
# match id:
# case int():
# query = query.filter(cls.id == id)
# limit = 1
# case _:
# pass
# match name:
# case str():
# query = query.filter(cls.name == name)
# limit = 1
# case _:
# pass
# return cls.execute_query(query=query, limit=limit)

View File

@@ -15,7 +15,7 @@ from operator import itemgetter
from pprint import pformat
from pandas import DataFrame
from sqlalchemy.ext.hybrid import hybrid_property
from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin
from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin, Procedure, kittype_procedure
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified
@@ -34,9 +34,8 @@ from jinja2.exceptions import TemplateNotFound
from jinja2 import Template
from PIL import Image
if TYPE_CHECKING:
from backend.db.models.kits import ProcedureType
from backend.db.models.kits import ProcedureType, Procedure
from . import kittype_procedure
logger = logging.getLogger(f"procedure.{__name__}")
@@ -233,7 +232,7 @@ class ClientSubmission(BaseClass, LogMixin):
# dicto, _ = self.kittype.construct_xl_map_for_use(self.proceduretype)
# sample = self.generate_associations(name="clientsubmissionsampleassociation")
samples = None
runs = [item.to_dict() for item in self.run]
runs = [item.to_dict(full_data=True) for item in self.run]
# custom = self.custom
else:
samples = None
@@ -263,6 +262,7 @@ class ClientSubmission(BaseClass, LogMixin):
output["contact_phone"] = contact_phone
# output["custom"] = custom
output["run"] = runs
output['name'] = self.name
return output
def add_sample(self, sample: Sample):
@@ -539,12 +539,14 @@ class Run(BaseClass, LogMixin):
samples = self.generate_associations(name="clientsubmissionsampleassociation")
equipment = self.generate_associations(name="submission_equipment_associations")
tips = self.generate_associations(name="submission_tips_associations")
procedures = [item.to_dict(full_data=True) for item in self.procedure]
custom = self.custom
else:
samples = None
equipment = None
tips = None
custom = None
procedures = None
try:
comments = self.comment
except Exception as e:
@@ -570,6 +572,8 @@ class Run(BaseClass, LogMixin):
output["contact"] = contact
output["contact_phone"] = contact_phone
output["custom"] = custom
output['procedures'] = procedures
output['name'] = self.name
try:
output["completed_date"] = self.completed_date.strftime("%Y-%m-%d")
except AttributeError:
@@ -1131,7 +1135,10 @@ class Run(BaseClass, LogMixin):
logger.debug(f"Got ProcedureType: {procedure_type}")
dlg = ProcedureCreation(parent=obj, run=self, proceduretype=procedure_type)
if dlg.exec():
pass
sql, _ = dlg.return_sql()
logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}")
sql.save()
def delete(self, obj=None):
@@ -1314,8 +1321,9 @@ class Run(BaseClass, LogMixin):
background_color="#6ffe1d"))
padded_list = []
for iii in range(1, proceduretype.total_wells+1):
row, column = proceduretype.ranked_plate[iii]
sample = next((item for item in ranked_samples if item['submission_rank']==iii),
dict(well_id=f"blank_{iii}", sample_id="", row=0, column=0, submission_rank=iii, background_color="#ffffff")
dict(well_id=f"blank_{iii}", sample_id="", row=row, column=column, 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'))))}")
@@ -1361,6 +1369,14 @@ class Sample(BaseClass, LogMixin):
run = association_proxy("samplerunassociation", "run") #: proxy of associated procedure
sampleprocedureassociation = relationship(
"ProcedureSampleAssociation",
back_populates="sample",
cascade="all, delete-orphan",
)
procedure = association_proxy("sampleprocedureassociation", "procedure")
@hybrid_property
def name(self):
return self.sample_id
@@ -1806,10 +1822,10 @@ class RunSampleAssociation(BaseClass):
"""
# id = Column(INTEGER, unique=True, nullable=False) #: id to be used for inheriting purposes
sample_id = Column(INTEGER, ForeignKey("_sample.id"), nullable=False) #: id of associated sample
sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated sample
run_id = Column(INTEGER, ForeignKey("_run.id"), primary_key=True) #: id of associated procedure
row = Column(INTEGER, primary_key=True) #: row on the 96 well plate
column = Column(INTEGER, primary_key=True) #: column on the 96 well plate
row = Column(INTEGER) #: row on the 96 well plate
column = Column(INTEGER) #: column on the 96 well plate
# misc_info = Column(JSON)
# NOTE: reference to the Submission object
@@ -2009,3 +2025,16 @@ class RunSampleAssociation(BaseClass):
def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}")
class ProcedureSampleAssociation(BaseClass):
procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure
sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated equipment
row = Column(INTEGER)
column = Column(INTEGER)
procedure = relationship(Procedure,
back_populates="proceduresampleassociation") #: associated procedure
sample = relationship(Sample, back_populates="sampleprocedureassociation") #: associated equipment

View File

@@ -105,7 +105,6 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
dicto = self.improved_dict(dictionaries=False)
logger.debug(f"Dicto: {dicto}")
sql, _ = self._sql_object().query_or_create(**dicto)
return sql
@@ -1383,8 +1382,6 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
value = {item.name: item.reagents for item in kittype.reagentrole}
return value
def update_kittype_reagentroles(self, kittype: str | KitType):
if kittype == self.__class__.model_fields['kittype'].default['value']:
return
@@ -1395,20 +1392,60 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
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)))
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:
if sample_dict['sample_id'].startswith("blank_"):
continue
row, column = self.proceduretype.ranked_plate[sample_dict['index']]
logger.debug(f"Row: {row}, Column: {column}")
try:
sample = next(
(item for item in self.samples if item.sample_id.upper() == sample_dict['sample_id'].upper()))
except StopIteration:
# NOTE: Code to check for added controls.
logger.debug(f"Sample not found by name: {sample_dict['sample_id']}, checking row {row} column {column}")
try:
sample = next(
(item for item in self.samples if item.row == row and item.column == column))
except StopIteration:
logger.error(f"Couldn't find sample: {pformat(sample_dict)}")
continue
logger.debug(f"Sample of interest: {sample.improved_dict()}")
sample.sample_id = sample_dict['sample_id']
sample.well_id = sample_dict['sample_id']
sample.row = row
sample.column = column
logger.debug(f"Updated samples:\n{pformat(self.samples)}")
def to_sql(self):
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
sql = super().to_sql()
if self.run:
sql.run = self.run
if self.proceduretype:
sql.proceduretype = self.proceduretype
for sample in self.samples:
if sample.sample_id.startswith("blank_") or sample.sample_id == "":
continue
sample_sql = sample.to_sql()
if sql.run:
if sample_sql not in sql.run.sample:
logger.debug(f"sample {sample_sql} not found in {sql.run.sample}")
run_assoc = RunSampleAssociation(sample=sample_sql, run=self.run, row=sample.row, column=sample.column)
else:
logger.debug(f"sample {sample_sql} found in {sql.run.sample}")
proc_assoc = ProcedureSampleAssociation(procedure=sql, sample=sample_sql, row=sample.row, column=sample.column)
if self.kittype['value'] not in ["NA", None, ""]:
kittype = KitType.query(name=self.kittype['value'], limit=1)
if kittype:
sql.kittype = kittype
return sql, None
class PydClientSubmission(PydBaseClass):
# sql_object: ClassVar = ClientSubmission

View File

@@ -12,7 +12,7 @@ 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 PyQt6.QtWidgets import QDialog, QGridLayout, QMenu, QDialogButtonBox
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
@@ -54,6 +54,11 @@ class ProcedureCreation(QDialog):
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):
html = render_details_template(
@@ -71,9 +76,9 @@ class ProcedureCreation(QDialog):
@pyqtSlot(str, str)
def text_changed(self, key: str, new_value: str):
# logger.debug(f"New value for {key}: {new_value}")
logger.debug(f"New value for {key}: {new_value}")
attribute = getattr(self.created_procedure, key)
attribute['value'] = new_value
attribute['value'] = new_value.strip('\"')
@pyqtSlot(str, bool)
def check_toggle(self, key: str, ischecked: bool):
@@ -94,6 +99,9 @@ class ProcedureCreation(QDialog):
def log(self, logtext: str):
logger.debug(logtext)
def return_sql(self):
return self.created_procedure.to_sql()
# class ProcedureWebViewer(QWebEngineView):
#

View File

@@ -9,7 +9,9 @@ from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, Q
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent
from typing import Dict, List
from backend import Procedure
from backend.db.models import Run, ClientSubmission
from tools import Report, Result, report_result
from .functions import select_open_file
@@ -263,8 +265,8 @@ class SubmissionsTree(QTreeView):
self.setIndentation(0)
self.setExpandsOnDoubleClick(False)
self.clicked.connect(self.on_clicked)
delegate = ClientSubmissionDelegate(self)
self.setItemDelegateForColumn(0, delegate)
delegate1 = ClientSubmissionDelegate(self)
self.setItemDelegateForColumn(0, delegate1)
self.model = model
self.setModel(self.model)
# self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents)
@@ -329,11 +331,49 @@ class SubmissionsTree(QTreeView):
ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
logger.debug(f"setting data:\n {pformat(self.data)}")
# sys.exit()
root = self.model.invisibleRootItem()
for submission in self.data:
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
group_item = self.model.add_group(group_str, query_str=submission['submitter_plate_id'])
submission_item = self.model.add_group(parent=root, item_data=dict(
name=group_str,
query_str=submission['submitter_plate_id'],
item_type=ClientSubmission
))
for run in submission['run']:
self.model.append_element_to_group(group_item=group_item, element=run)
# self.model.append_element_to_group(group_item=group_item, element=run)
run_item = self.model.add_group(parent=submission_item, item_data=dict(
name=run['plate_number'],
query_str=run['plate_number'],
item_type=Run
))
for procedure in run['procedures']:
self.model.add_group(parent=run_item, item_data=dict(
name=procedure['name'],
query_str=procedure['name'],
item_type=Procedure
))
# root = self.model.invisibleRootItem()
# for submission in self.data:
# submission_item = QStandardItem(submission['name'])
# root.appendRow(submission_item)
# for run in submission['run']:
# run_item = QStandardItem(run['name'])
# submission_item.appendRow(run_item)
# for procedure in run['procedures']:
# procedure_item = QStandardItem(procedure['name'])
# run_item.appendRow(procedure_item)
# self._populateTree(self.data, self.model.invisibleRootItem())
def _populateTree(self, children, parent):
for child in children:
logger.debug(child)
child_item = QStandardItem(child['name'])
parent.appendRow(child_item)
if isinstance(children, List):
self._populateTree(child, child_item)
def clear(self):
if self.model != None:
@@ -366,27 +406,26 @@ class ClientSubmissionRunModel(QStandardItemModel):
self.setColumnCount(len(headers))
self.setHorizontalHeaderLabels(headers)
def add_group(self, item_name, query_str: str):
def add_group(self, parent, item_data):
item_root = QStandardItem()
item_root.setEditable(False)
item = QStandardItem(item_name)
item = QStandardItem(item_data['name'])
item.setEditable(False)
ii = self.invisibleRootItem()
i = ii.rowCount()
i = parent.rowCount()
for j, it in enumerate((item_root, item)):
# NOTE: Adding item to invisible root row i, column j (wherever j comes from)
ii.setChild(i, j, it)
ii.setEditable(False)
parent.setChild(i, j, it)
parent.setEditable(False)
for j in range(self.columnCount()):
it = ii.child(i, j)
it = parent.child(i, j)
if it is None:
# NOTE: Set invisible root child to empty if it is None.
it = QStandardItem()
ii.setChild(i, j, it)
item_root.setData(dict(item_type=ClientSubmission, query_str=query_str), 1)
parent.setChild(i, j, it)
item_root.setData(dict(item_type=item_data['item_type'], query_str=item_data['query_str']), 1)
return item_root
def append_element_to_group(self, group_item, element: dict):
def append_element_to_group(self, group_item, item_data: dict):
# logger.debug(f"Element: {pformat(element)}")
j = group_item.rowCount()
item_icon = QStandardItem()
@@ -402,14 +441,15 @@ class ClientSubmissionRunModel(QStandardItemModel):
key = None
if not key:
continue
value = str(element[key])
value = str(item_data[key])
item = QStandardItem(value)
item.setBackground(QColor("#CFE2F3"))
item.setEditable(False)
# item_icon.setChild(j, i, item)
item.setData(dict(item_type=Run, query_str=element['plate_number']),1)
item.setData(dict(item_type=item_data['item_type'], query_str=item_data['query_str']),1)
group_item.setChild(j, i, item)
# group_item.setChild(j, 1, QStandardItem("B"))
return item
def get_value(self, idx: int, column: int = 1):
return self.item(idx, column)

View File

@@ -85,6 +85,27 @@ div.gallery {
object-fit: contain;
}
.context-menu {
display: none;
flex-direction: column;
align-items: flex-start;
width: 150px;
margin: 32px;
position: absolute;
}
.context-menu--active {
display: block;
z-index: 10;
background-color: white;
border: 2px solid black;
padding: 5px;
}
.plate {
display: inline-grid;
grid-auto-flow: column;
@@ -111,15 +132,17 @@ div.gallery {
border-radius: 8px;
}
.context-menu {
display: none;
position: absolute;
z-index: 10;
background-color: rgba(229, 231, 228, 0.7);
border-radius: 2px;
border-color: black;
ul.no-bullets {
list-style-type: none; /* Remove bullets */
padding: 0; /* Remove padding */
margin: 0; /* Remove margins */
}
.context-menu--active {
display: block;
.negativecontrol {
background-color: cyan;
}
.positivecontrol {
background-color: pink;
}

View File

@@ -12,7 +12,7 @@
// 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(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 =
@@ -116,7 +116,7 @@ function menuItemListener( link ) {
insertSample(contextIndex, task_id);
break;
case "InsertControl":
insertControl(contextIndex);
insertControl(taskItemInContext);
break;
case "RemoveSample":
removeSample(contextIndex);
@@ -125,6 +125,7 @@ function menuItemListener( link ) {
backend.log("default");
break;
}
rearrange_plate();
toggleMenuOff();
}
@@ -149,7 +150,7 @@ var clickCoordsX;
var clickCoordsY;
var menu = document.getElementById(contextMenuClassName);
var menuItems = menu.getElementsByClassName(contextMenuItemClassName);
var menuHeader = document.getElementById("menu-header");
const menuHeader = document.getElementById("menu-header");
var menuState = 0;
var menuWidth;
var menuHeight;
@@ -158,6 +159,7 @@ var menuPositionX;
var menuPositionY;
var windowWidth;
var windowHeight;
/**
* Initialise our application's code.
*/
@@ -192,6 +194,7 @@ function contextListener() {
function clickListener() {
document.addEventListener( "click", function(e) {
var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName );
backend.log(e.target.id)
if ( clickeElIsLink ) {
e.preventDefault();
menuItemListener( clickeElIsLink );
@@ -241,17 +244,39 @@ function insertSample( index ) {
backend.log( "Index - " + index + ", InsertSample");
}
function insertControl( index ) {
backend.log( "Index - " + index + ", InsertEN");
function insertControl( targetItem ) {
const gridC = document.getElementById("plate-container");
var existing_ens = document.getElementsByClassName("EN");
backend.log(existing_ens.length);
var en_num = existing_ens.length + 1;
const en_name = "EN" + en_num + "-" + rsl_plate_num;
var elem = document.createElement("div");
elem.setAttribute("id", en_name);
elem.setAttribute("class", "well negativecontrol EN");
elem.setAttribute("draggable", "true");
elem.innerHTML = '<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">' + en_name + '</p>'
gridC.insertBefore(elem, targetItem.nextSibling);
targetItem.remove();
}
function removeSample( index ) {
backend.log( "Index - " + index + ", RemoveSample");
function removeSample( targetItem ) {
const gridC = document.getElementById("plate-container");
var existing_wells = document.getElementsByClassName("well");
var en_num = existing_wells.length + 1;
var well_name = "blank_" + en_num;
var elem = document.createElement("div");
elem.setAttribute("id", well_name);
elem.setAttribute("class", "well");
elem.setAttribute("draggable", "true");
elem.innerHTML = '<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;"></p>'
gridC.insertBefore(elem, targetItem.nextSibling);
targetItem.remove();
}
/**
* Run the app.
*/

View File

@@ -1,51 +1,55 @@
const gridContainer = document.getElementById("plate-container");
let draggedItem = null;
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 start
gridContainer.addEventListener("dragstart", (e) => {
//Handle Drag End
gridContainer.addEventListener("dragend", (e) => {
e.target.style.opacity = "1";
draggedItem = null;
});
draggedItem = e.target;
draggedItem.style.opacity = "0.5";
});
//handle dragging ove grid items
gridContainer.addEventListener("dragover", (e) => {
e.preventDefault();
});
//Handle Drag End
gridContainer.addEventListener("dragend", (e) => {
e.target.style.opacity = "1";
draggedItem = null;
});
//Handle Drop
gridContainer.addEventListener("drop", (e) => {
e.preventDefault();
//handle dragging ove grid items
gridContainer.addEventListener("dragover", (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);
//Handle Drop
gridContainer.addEventListener("drop", (e) => {
e.preventDefault();
} else {
backend.log_drag(draggedIndex.toString(), targetIndex.toString() + " Greater");
gridContainer.insertBefore(draggedItem, targetItem);
const targetItem = e.target;
}
output = [];
fullGrid = [...gridContainer.children];
fullGrid.forEach(function(item, index) {
output.push({sample_id: item.id, index: index + 1})
});
backend.rearrange_plate(output);
}
});
if (
targetItem &&
targetItem !== draggedItem //&&
//targetItem.classList.contains("well")
) {
backend.log(targetItem.id);
const draggedIndex = [...gridContainer.children].indexOf(draggedItem);
const targetIndex = [...gridContainer.children].indexOf(targetItem);
if (draggedIndex < targetIndex) {
backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Lesser");
gridContainer.insertBefore(draggedItem, targetItem.nextSibling);
} else {
backend.log(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);
rearrange_plate();
}
});

View File

@@ -50,6 +50,17 @@
</body>
{% block script %}
<script>
var rsl_plate_num = "{{ run['plate_number'] }}";
function rearrange_plate() {
output = [];
fullGrid = [...gridContainer.children];
fullGrid.forEach(function(item, index) {
output.push({sample_id: item.id, index: index + 1})
});
backend.rearrange_plate(output);
}
</script>
{{ super() }}
{% endblock %}

View File

@@ -1,6 +1,6 @@
<nav class="context-menu" id="context-menu">
<nav class="flexDiv context-menu" id="context-menu">
<h4 id="menu-header"></h4>
<ul class="context-menu__items">
<ul class="context-menu__items no-bullets">
<li class="context-menu__item">
<a href="#" class="context-menu__link" data-action="InsertSample">
<i class="fa fa-eye"></i> Insert Sample

View File

@@ -2,14 +2,6 @@
{% for sample in samples %}
<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>
<!-- <div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}-->
<!-- <span class="tooltiptext">{{ sample['tooltip'] }}</span>-->
<!-- </div>--
grid-column-start: {{sample['column']}};
grid-column-end: {{sample['column']}};
grid-row-start: {{sample['row']}};
grid-row-end: {{sample['row']}};-->
</div>
{% endfor %}
</div>