diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 5156f18..e6936c5 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -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) + diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index c83dc17..cc8fc33 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -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 + diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index e6801e8..bf2e58f 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -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 diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index aabf1d2..e1454fb 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -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): # diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 27d6882..f327af1 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -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) diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css index 11df3b1..305bf6d 100644 --- a/src/submissions/templates/css/styles.css +++ b/src/submissions/templates/css/styles.css @@ -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; +} + diff --git a/src/submissions/templates/js/context_menu.js b/src/submissions/templates/js/context_menu.js index 65a221c..cb6df05 100644 --- a/src/submissions/templates/js/context_menu.js +++ b/src/submissions/templates/js/context_menu.js @@ -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 = '
' + en_name + '
' + 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 = '' + gridC.insertBefore(elem, targetItem.nextSibling); + targetItem.remove(); } + + + /** * Run the app. */ diff --git a/src/submissions/templates/js/grid_drag.js b/src/submissions/templates/js/grid_drag.js index 39f87c7..49a4538 100644 --- a/src/submissions/templates/js/grid_drag.js +++ b/src/submissions/templates/js/grid_drag.js @@ -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); - } - }); \ No newline at end of file + 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(); + } +}); \ No newline at end of file diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index 73370a9..614c8f6 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -50,6 +50,17 @@