Getting results referencing ProcedureSampleAssociation

This commit is contained in:
lwark
2025-06-09 11:05:10 -05:00
parent db0b65b07b
commit 10d4c9f155
17 changed files with 476 additions and 208 deletions

View File

@@ -14,7 +14,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \ from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \
jinja_template_loading jinja_template_loading, ctx
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path from pathlib import Path
@@ -1053,6 +1053,7 @@ class ProcedureType(BaseClass):
reagent_map = Column(JSON) reagent_map = Column(JSON)
plate_columns = Column(INTEGER, default=0) plate_columns = Column(INTEGER, default=0)
plate_rows = Column(INTEGER, default=0) plate_rows = Column(INTEGER, default=0)
allowed_result_methods = Column(JSON)
procedure = relationship("Procedure", procedure = relationship("Procedure",
back_populates="proceduretype") #: Concrete control of this type. back_populates="proceduretype") #: Concrete control of this type.
@@ -1095,6 +1096,10 @@ class ProcedureType(BaseClass):
cascade="all, delete-orphan" cascade="all, delete-orphan"
) #: Association of tiproles ) #: Association of tiproles
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.allowed_result_methods = dict()
def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]: def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]:
""" """
Make a map of all locations for tips or equipment. Make a map of all locations for tips or equipment.
@@ -1223,9 +1228,10 @@ class ProcedureType(BaseClass):
class Procedure(BaseClass): class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)
_name = Column(String, unique=True) name = Column(String, unique=True)
repeat = Column(INTEGER, nullable=False) repeat = Column(INTEGER, nullable=False)
technician = Column(JSON) #: name of processing tech(s) technician = Column(String(64)) #: name of processing tech(s)
results = relationship("Results", back_populates="procedure", uselist=True)
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL", proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id", ondelete="SET NULL",
name="fk_PRO_proceduretype_id")) #: client lab id from _organizations)) name="fk_PRO_proceduretype_id")) #: client lab id from _organizations))
proceduretype = relationship("ProcedureType", back_populates="procedure") proceduretype = relationship("ProcedureType", back_populates="procedure")
@@ -1274,14 +1280,6 @@ class Procedure(BaseClass):
tips = association_proxy("proceduretipsassociation", tips = association_proxy("proceduretipsassociation",
"tips") "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') @validates('repeat')
def validate_repeat(self, key, value): def validate_repeat(self, key, value):
if value > 1: if value > 1:
@@ -1314,6 +1312,33 @@ class Procedure(BaseClass):
output['name'] = self.name output['name'] = self.name
return output return output
@property
def custom_context_events(self) -> dict:
"""
Creates dictionary of str:function to be passed to context menu
Returns:
dict: dictionary of functions
"""
names = ["Add Results", "Edit", "Add Comment", "Show Details", "Delete"]
return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names}
def add_results(self, obj, resultstype_name:str):
logger.debug(f"Add Results! {resultstype_name}")
def edit(self, obj):
logger.debug("Edit!")
def add_comment(self, obj):
logger.debug("Add Comment!")
def show_details(self, obj):
logger.debug("Show Details!")
def delete(self, obj):
logger.debug("Delete!")
class ProcedureTypeKitTypeAssociation(BaseClass): class ProcedureTypeKitTypeAssociation(BaseClass):
@@ -2634,3 +2659,33 @@ class ProcedureTipsAssociation(BaseClass):
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name) return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
class Results(BaseClass):
id = Column(INTEGER, primary_key=True)
result = Column(JSON)
procedure_id = Column(INTEGER, ForeignKey("_procedure.id", ondelete='SET NULL',
name="fk_RES_procedure_id"))
procedure = relationship("Procedure", back_populates="results")
assoc_id = Column(INTEGER, ForeignKey("_proceduresampleassociation.id", ondelete='SET NULL',
name="fk_RES_ASSOC_id"))
sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results")
_img = Column(String(128))
@property
def image(self) -> bytes:
dir = self.__directory_path__.joinpath("submission_imgs.zip")
try:
assert dir.exists()
except AssertionError:
raise FileNotFoundError(f"{dir} not found.")
logger.debug(f"Getting image from {self.__directory_path__}")
with zipfile.ZipFile(dir) as zf:
with zf.open(self._img) as f:
return f.read()
@image.setter
def image(self, value):
self._img = value

View File

@@ -16,7 +16,7 @@ from pprint import pformat
from pandas import DataFrame from pandas import DataFrame
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin, Procedure, kittype_procedure 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 import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table, Sequence
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@@ -2028,6 +2028,8 @@ class RunSampleAssociation(BaseClass):
class ProcedureSampleAssociation(BaseClass): class ProcedureSampleAssociation(BaseClass):
id = Column(INTEGER, primary_key=True)
procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure 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 sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated equipment
row = Column(INTEGER) row = Column(INTEGER)
@@ -2038,3 +2040,29 @@ class ProcedureSampleAssociation(BaseClass):
sample = relationship(Sample, back_populates="sampleprocedureassociation") #: associated equipment sample = relationship(Sample, back_populates="sampleprocedureassociation") #: associated equipment
results = relationship("Results", back_populates="sampleprocedureassociation")
# def __init__(self, new_id:int|None=None, **kwarg):
# if new_id:
# self.id = new_id
# else:
# self.id = self.__class__.autoincrement_id()
# # new_id = self.__class__.autoincrement_id()
# super().__init__(**kwarg)
@classmethod
def autoincrement_id(cls) -> int:
"""
Increments the association id automatically
Returns:
int: incremented id
"""
try:
return max([item.id for item in cls.query()]) + 1
except ValueError as e:
logger.error(f"Problem incrementing id: {e}")
return 1

View File

@@ -1,26 +1,25 @@
""" """
""" """
import logging, re
from pathlib import Path from pathlib import Path
from typing import Generator, Tuple
from openpyxl import load_workbook from openpyxl import load_workbook
from pandas import DataFrame
from backend.validators import pydant from backend.validators import pydant
logger = logging.getLogger(f"submissions.{__name__}")
class DefaultParser(object): class DefaultParser(object):
default_range_dict = dict(
start_row=2,
end_row=18,
key_column=1,
value_column=2,
sheet="Sample List"
)
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}<{self.filepath.stem}>" return f"{self.__class__.__name__}<{self.filepath.stem}>"
def __init__(self, filepath: Path | str, range_dict: dict | None = None): def __init__(self, filepath: Path | str, range_dict: dict | None = None, *args, **kwargs):
try:
self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}") self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}")
except AttributeError:
self._pyd_object = pydant.PydResults
if isinstance(filepath, str): if isinstance(filepath, str):
self.filepath = Path(filepath) self.filepath = Path(filepath)
else: else:
@@ -30,5 +29,63 @@ class DefaultParser(object):
self.range_dict = self.__class__.default_range_dict self.range_dict = self.__class__.default_range_dict
else: else:
self.range_dict = range_dict self.range_dict = range_dict
for item in self.range_dict:
item['worksheet'] = self.workbook[item['sheet']]
def to_pydantic(self):
data = {key: value for key, value in self.parsed_info}
data['filepath'] = self.filepath
return self._pyd_object(**data)
class DefaultKEYVALUEParser(DefaultParser):
default_range_dict = [dict(
start_row=2,
end_row=18,
key_column=1,
value_column=2,
sheet="Sample List"
)]
@property
def parsed_info(self) -> Generator[Tuple, None, None]:
for item in self.range_dict:
rows = range(item['start_row'], item['end_row'] + 1)
for row in rows:
key = item['worksheet'].cell(row, item['key_column']).value
if key:
# Note: Remove anything in brackets.
key = re.sub(r"\(.*\)", "", key)
key = key.lower().replace(":", "").strip().replace(" ", "_")
value = item['worksheet'].cell(row, item['value_column']).value
value = dict(value=value, missing=False if value else True)
yield key, value
class DefaultTABLEParser(DefaultParser):
default_range_dict = [dict(
header_row=20,
end_row=116,
sheet="Sample List"
)]
@property
def parsed_info(self):
for item in self.range_dict:
list_worksheet = self.workbook[item['sheet']]
list_df = DataFrame([item for item in list_worksheet.values][item['header_row'] - 1:])
list_df.columns = list_df.iloc[0]
list_df = list_df[1:]
list_df = list_df.dropna(axis=1, how='all')
for ii, row in enumerate(list_df.iterrows()):
output = {key.lower().replace(" ", "_"): value for key, value in row[1].to_dict().items()}
yield output
def to_pydantic(self, **kwargs):
return [self._pyd_object(**output) for output in self.parsed_info]
from .submission_parser import * from .submission_parser import *

View File

@@ -0,0 +1,99 @@
"""
"""
import logging, re, sys
from pprint import pformat
from pathlib import Path
from typing import Generator, Tuple
from openpyxl import load_workbook
from backend.db.models import Run, Sample
from . import DefaultKEYVALUEParser, DefaultTABLEParser
logger = logging.getLogger(f"submissions.{__name__}")
class PCRSampleParser(DefaultTABLEParser):
"""Object to pull data from Design and Analysis PCR export file."""
default_range_dict = [dict(
header_row=25,
sheet="Results"
)]
@property
def parsed_info(self):
output = [item for item in super().parsed_info]
merge_column = "sample"
sample_names = list(set([item['sample'] for item in output]))
for sample in sample_names:
multi = dict()
sois = (item for item in output if item['sample']==sample)
for soi in sois:
multi[soi['target']] = {k:v for k, v in soi.items() if k != "target"}
yield (sample, multi)
def to_pydantic(self):
for key, sample_info in self.parsed_info:
sample_obj = Sample.query(sample_id=key)
if sample_obj and not isinstance(sample_obj, list):
yield self._pyd_object(results=sample_info, parent=sample_obj)
else:
continue
class PCRInfoParser(DefaultKEYVALUEParser):
default_range_dict = [dict(
start_row=1,
end_row=24,
key_column=1,
value_column=2,
sheet="Results"
)]
# def __init__(self, filepath: Path | str, range_dict: dict | None = None):
# super().__init__(filepath=filepath, range_dict=range_dict)
# self.worksheet = self.workbook[self.range_dict['sheet']]
# self.rows = range(self.range_dict['start_row'], self.range_dict['end_row'] + 1)
#
# @property
# def parsed_info(self) -> Generator[Tuple, None, None]:
# for row in self.rows:
# key = self.worksheet.cell(row, self.range_dict['key_column']).value
# if key:
# key = re.sub(r"\(.*\)", "", key)
# key = key.lower().replace(":", "").strip().replace(" ", "_")
# value = self.worksheet.cell(row, self.range_dict['value_column']).value
# value = dict(value=value, missing=False if value else True)
# yield key, value
#
def to_pydantic(self):
from backend.db.models import Procedure
data = {key: value for key, value in self.parsed_info}
data['filepath'] = self.filepath
return self._pyd_object(**data, parent=Procedure)
# @property
# def pcr_info(self) -> dict:
# """
# Parse general info rows for all types of PCR results
# """
# info_map = self.submission_obj.get_submission_type().sample_map['pcr_general_info']
# sheet = self.xl[info_map['sheet']]
# iter_rows = sheet.iter_rows(min_row=info_map['start_row'], max_row=info_map['end_row'])
# pcr = {}
# for row in iter_rows:
# try:
# key = row[0].value.lower().replace(' ', '_')
# except AttributeError as e:
# logger.error(f"No key: {row[0].value} due to {e}")
# continue
# value = row[1].value or ""
# pcr[key] = value
# pcr['imported_by'] = getuser()
# return pcr

View File

@@ -1,66 +1,50 @@
""" """
""" """
import logging, re import logging
from pathlib import Path from string import ascii_lowercase
from typing import Generator, Tuple from typing import Generator
from pandas import DataFrame from tools import row_keys
from . import DefaultKEYVALUEParser, DefaultTABLEParser
from . import DefaultParser
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class ClientSubmissionParser(DefaultParser): class ClientSubmissionParser(DefaultKEYVALUEParser):
""" """
Object for retrieving submitter info from "sample list" sheet Object for retrieving submitter info from "sample list" sheet
""" """
def __init__(self, filepath: Path | str, range_dict: dict | None = None): default_range_dict = [dict(
super().__init__(filepath=filepath, range_dict=range_dict) start_row=2,
self.worksheet = self.workbook[self.range_dict['sheet']] end_row=18,
self.rows = range(self.range_dict['start_row'], self.range_dict['end_row'] + 1) key_column=1,
value_column=2,
@property sheet="Sample List"
def parsed_info(self) -> Generator[Tuple, None, None]: )]
for row in self.rows:
key = self.worksheet.cell(row, self.range_dict['key_column']).value
if key:
key = re.sub(r"\(.*\)", "", key)
key = key.lower().replace(":", "").strip().replace(" ", "_")
value = self.worksheet.cell(row, self.range_dict['value_column']).value
value = dict(value=value, missing=False if value else True)
yield key, value
def to_pydantic(self):
data = {key: value for key, value in self.parsed_info}
data['filepath'] = self.filepath
return self._pyd_object(**data)
class SampleParser(DefaultParser):
class SampleParser(DefaultTABLEParser):
""" """
Object for retrieving submitter info from "sample list" sheet Object for retrieving submitter info from "sample list" sheet
""" """
default_range_dict = dict( default_range_dict = [dict(
header_row=20, header_row=20,
end_row=116, end_row=116,
list_sheet="Sample List" sheet="Sample List"
) )]
def __init__(self, filepath: Path | str, range_dict: dict | None = None):
super().__init__(filepath=filepath, range_dict=range_dict)
self.list_worksheet = self.workbook[self.range_dict['list_sheet']]
self.list_df = DataFrame([item for item in self.list_worksheet.values][self.range_dict['header_row'] - 1:])
self.list_df.columns = self.list_df.iloc[0]
self.list_df = self.list_df[1:]
self.list_df = self.list_df.dropna(axis=1, how='all')
@property @property
def parsed_info(self) -> Generator[dict, None, None]: def parsed_info(self) -> Generator[dict, None, None]:
for ii, row in enumerate(self.list_df.iterrows()): output = super().parsed_info
sample = {key.lower().replace(" ", "_"): value for key, value in row[1].to_dict().items()} for ii, sample in enumerate(output):
if isinstance(sample["row"], str) and sample["row"].lower() in ascii_lowercase[0:8]:
try:
sample["row"] = row_keys[sample["row"]]
except KeyError:
pass
sample['submission_rank'] = ii + 1 sample['submission_rank'] = ii + 1
yield sample yield sample

View File

@@ -208,4 +208,4 @@ class RSLNamer(object):
from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults

View File

@@ -780,7 +780,7 @@ class PydSubmission(BaseModel, extra='allow'):
return missing_info, missing_reagents return missing_info, missing_reagents
@report_result @report_result
def to_sql(self) -> Tuple[BasicRun | None, Report]: def to_sql(self) -> Tuple[Run | None, Report]:
""" """
Converts this instance into a backend.db.models.procedure.BasicRun instance Converts this instance into a backend.db.models.procedure.BasicRun instance
@@ -791,7 +791,7 @@ class PydSubmission(BaseModel, extra='allow'):
dicto = self.improved_dict() dicto = self.improved_dict()
# logger.debug(f"Pydantic procedure type: {self.proceduretype['value']}") # logger.debug(f"Pydantic procedure type: {self.proceduretype['value']}")
# logger.debug(f"Pydantic improved_dict: {pformat(dicto)}") # logger.debug(f"Pydantic improved_dict: {pformat(dicto)}")
instance, result = BasicRun.query_or_create(submissiontype=self.submission_type['value'], instance, result = Run.query_or_create(submissiontype=self.submission_type['value'],
rsl_plate_num=self.rsl_plate_num['value']) rsl_plate_num=self.rsl_plate_num['value'])
# logger.debug(f"Created or queried instance: {instance}") # logger.debug(f"Created or queried instance: {instance}")
if instance is None: if instance is None:
@@ -1353,7 +1353,15 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
def rescue_name(cls, value, values): def rescue_name(cls, value, values):
if value['value'] == cls.model_fields['name'].default['value']: if value['value'] == cls.model_fields['name'].default['value']:
if values.data['proceduretype']: if values.data['proceduretype']:
value['value'] = values.data['proceduretype'].name procedure_type = values.data['proceduretype'].name
else:
procedure_type = None
if values.data['run']:
run = values.data['run'].rsl_plate_num
else:
run = None
value['value'] = f"{procedure_type}-{run}"
value['missing'] = True
return value return value
@field_validator("possible_kits") @field_validator("possible_kits")
@@ -1522,3 +1530,10 @@ class PydClientSubmission(PydBaseClass):
""" """
from frontend.widgets.submission_widget import ClientSubmissionFormWidget from frontend.widgets.submission_widget import ClientSubmissionFormWidget
return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable) return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable)
class PydResults(PydBaseClass, arbitrary_types_allowed=True):
results: dict = Field(default={})
parent: Sample|Procedure

View File

@@ -0,0 +1,26 @@
"""
"""
from pathlib import Path
from backend.validators import PydResults
from backend.db.models import Procedure, Results
from backend.excel.parsers.pcr_parser import PCRSampleParser, PCRInfoParser
from frontend.widgets.functions import select_open_file
from . import DefaultResults
class PCR(DefaultResults):
def __init__(self, procedure: Procedure, fname:Path|str|None=None):
self.procedure = procedure
if not fname:
self.fname = select_open_file(file_extension="xlsx")
elif isinstance(fname, str):
self.fname = Path(fname)
self.info_parser = PCRInfoParser(filepath=fname)
self.sample_parser = PCRSampleParser(filepath=fname)
def build_procedure(self):
results = PydResults(parent=self.procedure)
results.results =

View File

@@ -0,0 +1,7 @@
class DefaultResults(object):
pass
from .PCR import pcr

View File

@@ -231,27 +231,27 @@ class SubmissionsSheet(QTableView):
return report return report
class ClientSubmissionDelegate(QStyledItemDelegate): # class ClientSubmissionDelegate(QStyledItemDelegate):
#
def __init__(self, parent=None): # def __init__(self, parent=None):
super(ClientSubmissionDelegate, self).__init__(parent) # super(ClientSubmissionDelegate, self).__init__(parent)
pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton # pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
icon1 = QWidget().style().standardIcon(pixmapi) # icon1 = QWidget().style().standardIcon(pixmapi)
pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton # pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
icon2 = QWidget().style().standardIcon(pixmapi) # icon2 = QWidget().style().standardIcon(pixmapi)
self._plus_icon = icon1 # self._plus_icon = icon1
self._minus_icon = icon2 # self._minus_icon = icon2
#
def initStyleOption(self, option, index): # def initStyleOption(self, option, index):
super(ClientSubmissionDelegate, self).initStyleOption(option, index) # super(ClientSubmissionDelegate, self).initStyleOption(option, index)
if not index.parent().isValid(): # if not index.parent().isValid():
is_open = bool(option.state & QStyle.StateFlag.State_Open) # is_open = bool(option.state & QStyle.StateFlag.State_Open)
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration # option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
option.icon = self._minus_icon if is_open else self._plus_icon # option.icon = self._minus_icon if is_open else self._plus_icon
class RunDelegate(ClientSubmissionDelegate): # class RunDelegate(ClientSubmissionDelegate):
pass # pass
class SubmissionsTree(QTreeView): class SubmissionsTree(QTreeView):
@@ -262,11 +262,11 @@ class SubmissionsTree(QTreeView):
def __init__(self, model, parent=None): def __init__(self, model, parent=None):
super(SubmissionsTree, self).__init__(parent) super(SubmissionsTree, self).__init__(parent)
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count() self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
self.setIndentation(0) # self.setIndentation(0)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
self.clicked.connect(self.on_clicked) # self.clicked.connect(self.on_clicked)
delegate1 = ClientSubmissionDelegate(self) # delegate1 = ClientSubmissionDelegate(self)
self.setItemDelegateForColumn(0, delegate1) # self.setItemDelegateForColumn(0, delegate1)
self.model = model self.model = model
self.setModel(self.model) self.setModel(self.model)
# self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents) # self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents)
@@ -276,14 +276,42 @@ class SubmissionsTree(QTreeView):
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) # self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
# self.customContextMenuRequested.connect(self.open_menu) # self.customContextMenuRequested.connect(self.open_menu)
self.setStyleSheet("""
QTreeView {
background-color: #f5f5f5;
alternate-background-color: "#cfe2f3";
border: 1px solid #d3d3d3;
}
QTreeView::item {
padding: 5px;
border-bottom: 1px solid #d3d3d3;
}
QTreeView::item:selected {
background-color: #0078d7;
color: white;
}
""")
# Enable alternating row colors
self.setAlternatingRowColors(True)
self.setIndentation(20)
self.setItemsExpandable(True)
self.expanded.connect(self.expand_item)
for ii in range(2): for ii in range(2):
self.resizeColumnToContents(ii) self.resizeColumnToContents(ii)
@pyqtSlot(QModelIndex) # @pyqtSlot(QModelIndex)
def on_clicked(self, index): # def on_clicked(self, index):
if not index.parent().isValid() and index.column() == 0: # if not index.parent().isValid() and index.column() == 0:
self.setExpanded(index, not self.isExpanded(index)) # self.setExpanded(index, not self.isExpanded(index))
def expand_item(self, event: QModelIndex):
logger.debug(f"Data: {event.data()}")
logger.debug(f"Parent {event.parent().data()}")
logger.debug(f"Row: {event.row()}")
logger.debug(f"Sibling: {event.siblingAtRow(event.row()).data()}")
logger.debug(f"Model: {event.model().event()}")
def contextMenuEvent(self, event: QContextMenuEvent): def contextMenuEvent(self, event: QContextMenuEvent):
""" """
@@ -306,7 +334,9 @@ class SubmissionsTree(QTreeView):
self.menu = QMenu(self) self.menu = QMenu(self)
self.con_actions = query_obj.custom_context_events self.con_actions = query_obj.custom_context_events
for key in self.con_actions.keys(): for key in self.con_actions.keys():
if key.lower() == "add procedure": logger.debug(key)
match key.lower():
case "add procedure":
action = QMenu(self.menu) action = QMenu(self.menu)
action.setTitle("Add Procedure") action.setTitle("Add Procedure")
for procedure in query_obj.allowed_procedures: for procedure in query_obj.allowed_procedures:
@@ -315,7 +345,16 @@ class SubmissionsTree(QTreeView):
proc.triggered.connect(lambda _, procedure_name=proc_name: self.con_actions['Add Procedure'](obj=self, proceduretype_name=procedure_name)) proc.triggered.connect(lambda _, procedure_name=proc_name: self.con_actions['Add Procedure'](obj=self, proceduretype_name=procedure_name))
action.addAction(proc) action.addAction(proc)
self.menu.addMenu(action) self.menu.addMenu(action)
else: case "add results":
action = QMenu(self.menu)
action.setTitle("Add Results")
for results in query_obj.proceduretype.allowed_result_methods:
res_name = results
res = QAction(res_name, action)
res.triggered.connect(lambda _, procedure_name=res_name: self.con_actions['Add Results'](obj=self, resultstype_name=procedure_name))
action.addAction(res)
self.menu.addMenu(action)
case _:
action = QAction(key, self) action = QAction(key, self)
action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self)) action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self))
self.menu.addAction(action) self.menu.addAction(action)
@@ -334,36 +373,27 @@ class SubmissionsTree(QTreeView):
root = self.model.invisibleRootItem() root = self.model.invisibleRootItem()
for submission in self.data: for submission in self.data:
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}" group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
submission_item = self.model.add_group(parent=root, item_data=dict( submission_item = self.model.add_child(parent=root, child=dict(
name=group_str, name=group_str,
query_str=submission['submitter_plate_id'], query_str=submission['submitter_plate_id'],
item_type=ClientSubmission item_type=ClientSubmission
)) ))
logger.debug(f"Added {submission_item}")
for run in submission['run']: 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( run_item = self.model.add_child(parent=submission_item, child=dict(
name=run['plate_number'], name=run['plate_number'],
query_str=run['plate_number'], query_str=run['plate_number'],
item_type=Run item_type=Run
)) ))
logger.debug(f"Added {run_item}")
for procedure in run['procedures']: for procedure in run['procedures']:
self.model.add_group(parent=run_item, item_data=dict( procedure_item = self.model.add_child(parent=run_item, child=dict(
name=procedure['name'], name=procedure['name'],
query_str=procedure['name'], query_str=procedure['name'],
item_type=Procedure item_type=Procedure
)) ))
# root = self.model.invisibleRootItem() logger.debug(f"Added {procedure_item}")
# 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): def _populateTree(self, children, parent):
for child in children: for child in children:
@@ -381,14 +411,20 @@ class SubmissionsTree(QTreeView):
self.model.setRowCount(0) # works self.model.setRowCount(0) # works
def show_details(self, sel: QModelIndex): def show_details(self, sel: QModelIndex):
id = self.selectionModel().currentIndex() # id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0) # NOTE: Convert to data in id column (i.e. column 0)
id = id.sibling(id.row(), 1) # id = id.sibling(id.row(), 1)
try: indexes = self.selectedIndexes()
id = int(id.data())
except ValueError: dicto = next((item.data(1) for item in indexes if item.data(1)))
return logger.debug(dicto)
Run.query(id=id).show_details(self) # try:
# id = int(id.data())
# except ValueError:
# return
# Run.query(id=id).show_details(self)
obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(obj)
def link_extractions(self): def link_extractions(self):
pass pass
@@ -402,59 +438,18 @@ class ClientSubmissionRunModel(QStandardItemModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(ClientSubmissionRunModel, self).__init__(parent) super(ClientSubmissionRunModel, self).__init__(parent)
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"] # headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
self.setColumnCount(len(headers)) # self.setColumnCount(len(headers))
self.setHorizontalHeaderLabels(headers) # self.setHorizontalHeaderLabels(headers)
def add_group(self, parent, item_data):
item_root = QStandardItem()
item_root.setEditable(False)
item = QStandardItem(item_data['name'])
item.setEditable(False)
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)
parent.setChild(i, j, it)
parent.setEditable(False)
for j in range(self.columnCount()):
it = parent.child(i, j)
if it is None:
# NOTE: Set invisible root child to empty if it is None.
it = QStandardItem()
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, item_data: dict):
# logger.debug(f"Element: {pformat(element)}") def add_child(self, parent: QStandardItem, child:dict):
j = group_item.rowCount() item = QStandardItem(child['name'])
item_icon = QStandardItem() item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1)
item_icon.setEditable(False) parent.appendRow(item)
# item_icon.setBackground(QColor("#0D1225"))
# item_icon.setData(dict(item_type="Run", query_str=element['plate_number']), 1)
# group_item.setChild(j, 0, item_icon)
for i in range(self.columnCount()):
it = self.horizontalHeaderItem(i)
try:
key = it.text().lower().replace(" ", "_")
except AttributeError:
key = None
if not key:
continue
value = str(item_data[key])
item = QStandardItem(value)
item.setBackground(QColor("#CFE2F3"))
item.setEditable(False) item.setEditable(False)
# item_icon.setChild(j, i, item)
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 return item
def get_value(self, idx: int, column: int = 1): def edit_item(self):
return self.item(idx, column) pass
def query_group_object(self, idx: int):
row_obj = self.get_value(idx)
logger.debug(row_obj.query_str)
return self.sql_object.query(name=row_obj.query_str, limit=1)

View File

@@ -23,10 +23,10 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
var processSelection = document.getElementsByClassName('process'); var processSelection = document.getElementsByClassName('process');
for(let i = 0; i < processSelection.length; i++) { for(let i = 0; i < processSelection.length; i++) {
@@ -45,6 +45,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
}, false); }, false);
</script>
{% endblock %} {% endblock %}
</script>
</html> </html>

View File

@@ -23,10 +23,10 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
var equipmentSelection = document.getElementsByClassName('equipment'); var equipmentSelection = document.getElementsByClassName('equipment');
for(let i = 0; i < equipmentSelection.length; i++) { for(let i = 0; i < equipmentSelection.length; i++) {
@@ -44,6 +44,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
}, false); }, false);
</script>
{% endblock %} {% endblock %}
</script>
</html> </html>

View File

@@ -23,9 +23,10 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
document.getElementById("save_btn").addEventListener("click", function(){ document.getElementById("save_btn").addEventListener("click", function(){
var new_lot = document.getElementById('lot').value var new_lot = document.getElementById('lot').value
var new_exp = document.getElementById('expiry').value var new_exp = document.getElementById('expiry').value
@@ -39,6 +40,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
}, false); }, false);
</script>
{% endblock %} {% endblock %}
</script>
</html> </html>

View File

@@ -77,10 +77,10 @@
<br> <br>
<br> <br>
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
var sampleSelection = document.getElementsByClassName('sample'); var sampleSelection = document.getElementsByClassName('sample');
for(let i = 0; i < sampleSelection.length; i++) { for(let i = 0; i < sampleSelection.length; i++) {
@@ -129,7 +129,7 @@
document.getElementById("sign_btn").addEventListener("click", function(){ document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}"); backend.sign_off("{{ sub['plate_number'] }}");
}); });
{% endblock %}
</script> </script>
{% endblock %}
</html> </html>

View File

@@ -30,9 +30,10 @@
</form> </form>
{% endblock %} {% endblock %}
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
{% for sample in samples %} {% for sample in samples %}
document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){ document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value); backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
@@ -42,12 +43,6 @@
backend.enable_sample("{{ sample['submission_rank'] }}", this.checked); backend.enable_sample("{{ sample['submission_rank'] }}", this.checked);
}); });
{% endif %} {% endif %}
<!-- document.getElementById("{{ sample['submission_rank'] }}_row").addEventListener("input", function(){-->
<!-- backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);-->
<!-- });-->
<!-- document.getElementById("{{ sample['submission_rank'] }}_column").addEventListener("input", function(){-->
<!-- backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);-->
<!-- });-->
{% endfor %} {% endfor %}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
@@ -55,5 +50,5 @@
document.getElementById("rsl_plate_num").addEventListener("input", function(){ document.getElementById("rsl_plate_num").addEventListener("input", function(){
backend.set_rsl_plate_num(this.value); backend.set_rsl_plate_num(this.value);
}); });
</script>
{% endblock %} {% endblock %}
</script>

View File

@@ -21,9 +21,10 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
{% for submission in sample['submissions'] %} {% for submission in sample['submissions'] %}
document.getElementById("{{ submission['plate_name'] }}").addEventListener("click", function(){ document.getElementById("{{ submission['plate_name'] }}").addEventListener("click", function(){
backend.submission_details("{{ submission['plate_name'] }}"); backend.submission_details("{{ submission['plate_name'] }}");
@@ -32,6 +33,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
}, false); }, false);
{% endblock %}
</script> </script>
{% endblock %}
</html> </html>

View File

@@ -23,10 +23,10 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</body> </body>
<script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script>
var processSelection = document.getElementsByClassName('process'); var processSelection = document.getElementsByClassName('process');
for(let i = 0; i < processSelection.length; i++) { for(let i = 0; i < processSelection.length; i++) {
@@ -45,6 +45,7 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false); backend.activate_export(false);
}, false); }, false);
</script>
{% endblock %} {% endblock %}
</script>
</html> </html>