Getting results referencing ProcedureSampleAssociation
This commit is contained in:
@@ -14,7 +14,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from datetime import date, datetime, timedelta
|
||||
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 pandas import ExcelFile
|
||||
from pathlib import Path
|
||||
@@ -1053,6 +1053,7 @@ class ProcedureType(BaseClass):
|
||||
reagent_map = Column(JSON)
|
||||
plate_columns = Column(INTEGER, default=0)
|
||||
plate_rows = Column(INTEGER, default=0)
|
||||
allowed_result_methods = Column(JSON)
|
||||
|
||||
procedure = relationship("Procedure",
|
||||
back_populates="proceduretype") #: Concrete control of this type.
|
||||
@@ -1095,6 +1096,10 @@ class ProcedureType(BaseClass):
|
||||
cascade="all, delete-orphan"
|
||||
) #: 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]:
|
||||
"""
|
||||
Make a map of all locations for tips or equipment.
|
||||
@@ -1223,9 +1228,10 @@ 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)
|
||||
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",
|
||||
name="fk_PRO_proceduretype_id")) #: client lab id from _organizations))
|
||||
proceduretype = relationship("ProcedureType", back_populates="procedure")
|
||||
@@ -1274,14 +1280,6 @@ 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:
|
||||
@@ -1314,6 +1312,33 @@ class Procedure(BaseClass):
|
||||
output['name'] = self.name
|
||||
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):
|
||||
@@ -2634,3 +2659,33 @@ class ProcedureTipsAssociation(BaseClass):
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ 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, 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.attributes import flag_modified
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
@@ -2028,6 +2028,8 @@ class RunSampleAssociation(BaseClass):
|
||||
|
||||
|
||||
class ProcedureSampleAssociation(BaseClass):
|
||||
|
||||
id = Column(INTEGER, primary_key=True)
|
||||
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)
|
||||
@@ -2038,3 +2040,29 @@ class ProcedureSampleAssociation(BaseClass):
|
||||
|
||||
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
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
"""
|
||||
|
||||
"""
|
||||
import logging, re
|
||||
from pathlib import Path
|
||||
from typing import Generator, Tuple
|
||||
from openpyxl import load_workbook
|
||||
from pandas import DataFrame
|
||||
from backend.validators import pydant
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
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):
|
||||
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', '')}")
|
||||
except AttributeError:
|
||||
self._pyd_object = pydant.PydResults
|
||||
if isinstance(filepath, str):
|
||||
self.filepath = Path(filepath)
|
||||
else:
|
||||
@@ -30,5 +29,63 @@ class DefaultParser(object):
|
||||
self.range_dict = self.__class__.default_range_dict
|
||||
else:
|
||||
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 *
|
||||
99
src/submissions/backend/excel/parsers/pcr_parser.py
Normal file
99
src/submissions/backend/excel/parsers/pcr_parser.py
Normal 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
|
||||
@@ -1,66 +1,50 @@
|
||||
"""
|
||||
|
||||
"""
|
||||
import logging, re
|
||||
from pathlib import Path
|
||||
from typing import Generator, Tuple
|
||||
from pandas import DataFrame
|
||||
|
||||
from . import DefaultParser
|
||||
import logging
|
||||
from string import ascii_lowercase
|
||||
from typing import Generator
|
||||
from tools import row_keys
|
||||
from . import DefaultKEYVALUEParser, DefaultTABLEParser
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class ClientSubmissionParser(DefaultParser):
|
||||
class ClientSubmissionParser(DefaultKEYVALUEParser):
|
||||
"""
|
||||
Object for retrieving submitter info from "sample list" sheet
|
||||
"""
|
||||
|
||||
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):
|
||||
data = {key: value for key, value in self.parsed_info}
|
||||
data['filepath'] = self.filepath
|
||||
return self._pyd_object(**data)
|
||||
default_range_dict = [dict(
|
||||
start_row=2,
|
||||
end_row=18,
|
||||
key_column=1,
|
||||
value_column=2,
|
||||
sheet="Sample List"
|
||||
)]
|
||||
|
||||
|
||||
class SampleParser(DefaultParser):
|
||||
|
||||
class SampleParser(DefaultTABLEParser):
|
||||
"""
|
||||
Object for retrieving submitter info from "sample list" sheet
|
||||
"""
|
||||
|
||||
default_range_dict = dict(
|
||||
default_range_dict = [dict(
|
||||
header_row=20,
|
||||
end_row=116,
|
||||
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')
|
||||
sheet="Sample List"
|
||||
)]
|
||||
|
||||
@property
|
||||
def parsed_info(self) -> Generator[dict, None, None]:
|
||||
for ii, row in enumerate(self.list_df.iterrows()):
|
||||
sample = {key.lower().replace(" ", "_"): value for key, value in row[1].to_dict().items()}
|
||||
output = super().parsed_info
|
||||
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
|
||||
yield sample
|
||||
|
||||
|
||||
@@ -208,4 +208,4 @@ class RSLNamer(object):
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -780,7 +780,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
return missing_info, missing_reagents
|
||||
|
||||
@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
|
||||
|
||||
@@ -791,7 +791,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
dicto = self.improved_dict()
|
||||
# logger.debug(f"Pydantic procedure type: {self.proceduretype['value']}")
|
||||
# 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'])
|
||||
# logger.debug(f"Created or queried instance: {instance}")
|
||||
if instance is None:
|
||||
@@ -1353,7 +1353,15 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
|
||||
def rescue_name(cls, value, values):
|
||||
if value['value'] == cls.model_fields['name'].default['value']:
|
||||
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
|
||||
|
||||
@field_validator("possible_kits")
|
||||
@@ -1522,3 +1530,10 @@ class PydClientSubmission(PydBaseClass):
|
||||
"""
|
||||
from frontend.widgets.submission_widget import ClientSubmissionFormWidget
|
||||
return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable)
|
||||
|
||||
|
||||
class PydResults(PydBaseClass, arbitrary_types_allowed=True):
|
||||
|
||||
results: dict = Field(default={})
|
||||
parent: Sample|Procedure
|
||||
|
||||
|
||||
26
src/submissions/frontend/widgets/results/PCR.py
Normal file
26
src/submissions/frontend/widgets/results/PCR.py
Normal 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 =
|
||||
|
||||
|
||||
7
src/submissions/frontend/widgets/results/__init__.py
Normal file
7
src/submissions/frontend/widgets/results/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
class DefaultResults(object):
|
||||
|
||||
pass
|
||||
|
||||
from .PCR import pcr
|
||||
@@ -231,27 +231,27 @@ class SubmissionsSheet(QTableView):
|
||||
return report
|
||||
|
||||
|
||||
class ClientSubmissionDelegate(QStyledItemDelegate):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ClientSubmissionDelegate, self).__init__(parent)
|
||||
pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
|
||||
icon1 = QWidget().style().standardIcon(pixmapi)
|
||||
pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
|
||||
icon2 = QWidget().style().standardIcon(pixmapi)
|
||||
self._plus_icon = icon1
|
||||
self._minus_icon = icon2
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super(ClientSubmissionDelegate, self).initStyleOption(option, index)
|
||||
if not index.parent().isValid():
|
||||
is_open = bool(option.state & QStyle.StateFlag.State_Open)
|
||||
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
|
||||
option.icon = self._minus_icon if is_open else self._plus_icon
|
||||
# class ClientSubmissionDelegate(QStyledItemDelegate):
|
||||
#
|
||||
# def __init__(self, parent=None):
|
||||
# super(ClientSubmissionDelegate, self).__init__(parent)
|
||||
# pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
|
||||
# icon1 = QWidget().style().standardIcon(pixmapi)
|
||||
# pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
|
||||
# icon2 = QWidget().style().standardIcon(pixmapi)
|
||||
# self._plus_icon = icon1
|
||||
# self._minus_icon = icon2
|
||||
#
|
||||
# def initStyleOption(self, option, index):
|
||||
# super(ClientSubmissionDelegate, self).initStyleOption(option, index)
|
||||
# if not index.parent().isValid():
|
||||
# is_open = bool(option.state & QStyle.StateFlag.State_Open)
|
||||
# option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
|
||||
# option.icon = self._minus_icon if is_open else self._plus_icon
|
||||
|
||||
|
||||
class RunDelegate(ClientSubmissionDelegate):
|
||||
pass
|
||||
# class RunDelegate(ClientSubmissionDelegate):
|
||||
# pass
|
||||
|
||||
|
||||
class SubmissionsTree(QTreeView):
|
||||
@@ -262,11 +262,11 @@ class SubmissionsTree(QTreeView):
|
||||
def __init__(self, model, parent=None):
|
||||
super(SubmissionsTree, self).__init__(parent)
|
||||
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
|
||||
self.setIndentation(0)
|
||||
# self.setIndentation(0)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.clicked.connect(self.on_clicked)
|
||||
delegate1 = ClientSubmissionDelegate(self)
|
||||
self.setItemDelegateForColumn(0, delegate1)
|
||||
# self.clicked.connect(self.on_clicked)
|
||||
# delegate1 = ClientSubmissionDelegate(self)
|
||||
# self.setItemDelegateForColumn(0, delegate1)
|
||||
self.model = model
|
||||
self.setModel(self.model)
|
||||
# self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents)
|
||||
@@ -276,14 +276,42 @@ class SubmissionsTree(QTreeView):
|
||||
self.doubleClicked.connect(self.show_details)
|
||||
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
# 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):
|
||||
self.resizeColumnToContents(ii)
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def on_clicked(self, index):
|
||||
if not index.parent().isValid() and index.column() == 0:
|
||||
self.setExpanded(index, not self.isExpanded(index))
|
||||
# @pyqtSlot(QModelIndex)
|
||||
# def on_clicked(self, index):
|
||||
# if not index.parent().isValid() and index.column() == 0:
|
||||
# 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):
|
||||
"""
|
||||
@@ -306,7 +334,9 @@ class SubmissionsTree(QTreeView):
|
||||
self.menu = QMenu(self)
|
||||
self.con_actions = query_obj.custom_context_events
|
||||
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.setTitle("Add Procedure")
|
||||
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))
|
||||
action.addAction(proc)
|
||||
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.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self))
|
||||
self.menu.addAction(action)
|
||||
@@ -334,36 +373,27 @@ class SubmissionsTree(QTreeView):
|
||||
root = self.model.invisibleRootItem()
|
||||
for submission in self.data:
|
||||
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,
|
||||
query_str=submission['submitter_plate_id'],
|
||||
item_type=ClientSubmission
|
||||
))
|
||||
logger.debug(f"Added {submission_item}")
|
||||
for run in submission['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'],
|
||||
query_str=run['plate_number'],
|
||||
item_type=Run
|
||||
))
|
||||
|
||||
logger.debug(f"Added {run_item}")
|
||||
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'],
|
||||
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())
|
||||
logger.debug(f"Added {procedure_item}")
|
||||
|
||||
def _populateTree(self, children, parent):
|
||||
for child in children:
|
||||
@@ -381,14 +411,20 @@ class SubmissionsTree(QTreeView):
|
||||
self.model.setRowCount(0) # works
|
||||
|
||||
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)
|
||||
id = id.sibling(id.row(), 1)
|
||||
try:
|
||||
id = int(id.data())
|
||||
except ValueError:
|
||||
return
|
||||
Run.query(id=id).show_details(self)
|
||||
# id = id.sibling(id.row(), 1)
|
||||
indexes = self.selectedIndexes()
|
||||
|
||||
dicto = next((item.data(1) for item in indexes if item.data(1)))
|
||||
logger.debug(dicto)
|
||||
# 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):
|
||||
pass
|
||||
@@ -402,59 +438,18 @@ class ClientSubmissionRunModel(QStandardItemModel):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ClientSubmissionRunModel, self).__init__(parent)
|
||||
headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
|
||||
self.setColumnCount(len(headers))
|
||||
self.setHorizontalHeaderLabels(headers)
|
||||
# headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
|
||||
# self.setColumnCount(len(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)}")
|
||||
j = group_item.rowCount()
|
||||
item_icon = QStandardItem()
|
||||
item_icon.setEditable(False)
|
||||
# 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"))
|
||||
|
||||
def add_child(self, parent: QStandardItem, child:dict):
|
||||
item = QStandardItem(child['name'])
|
||||
item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1)
|
||||
parent.appendRow(item)
|
||||
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
|
||||
|
||||
def get_value(self, idx: int, column: int = 1):
|
||||
return self.item(idx, column)
|
||||
|
||||
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)
|
||||
def edit_item(self):
|
||||
pass
|
||||
@@ -23,10 +23,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script>
|
||||
var processSelection = document.getElementsByClassName('process');
|
||||
|
||||
for(let i = 0; i < processSelection.length; i++) {
|
||||
@@ -45,6 +45,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
</script>
|
||||
{% endblock %}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -23,10 +23,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script>
|
||||
var equipmentSelection = document.getElementsByClassName('equipment');
|
||||
|
||||
for(let i = 0; i < equipmentSelection.length; i++) {
|
||||
@@ -44,6 +44,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
</script>
|
||||
{% endblock %}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -23,9 +23,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.getElementById("save_btn").addEventListener("click", function(){
|
||||
var new_lot = document.getElementById('lot').value
|
||||
var new_exp = document.getElementById('expiry').value
|
||||
@@ -39,6 +40,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
</script>
|
||||
{% endblock %}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -77,10 +77,10 @@
|
||||
<br>
|
||||
<br>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script>
|
||||
var sampleSelection = document.getElementsByClassName('sample');
|
||||
|
||||
for(let i = 0; i < sampleSelection.length; i++) {
|
||||
@@ -129,7 +129,7 @@
|
||||
document.getElementById("sign_btn").addEventListener("click", function(){
|
||||
backend.sign_off("{{ sub['plate_number'] }}");
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
</html>
|
||||
|
||||
@@ -30,9 +30,10 @@
|
||||
</form>
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
{% for sample in samples %}
|
||||
document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){
|
||||
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
|
||||
@@ -42,12 +43,6 @@
|
||||
backend.enable_sample("{{ sample['submission_rank'] }}", this.checked);
|
||||
});
|
||||
{% 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 %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
@@ -55,5 +50,5 @@
|
||||
document.getElementById("rsl_plate_num").addEventListener("input", function(){
|
||||
backend.set_rsl_plate_num(this.value);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</script>
|
||||
@@ -21,9 +21,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
{% for submission in sample['submissions'] %}
|
||||
document.getElementById("{{ submission['plate_name'] }}").addEventListener("click", function(){
|
||||
backend.submission_details("{{ submission['plate_name'] }}");
|
||||
@@ -32,6 +33,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
{% endblock %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
</html>
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script>
|
||||
var processSelection = document.getElementsByClassName('process');
|
||||
|
||||
for(let i = 0; i < processSelection.length; i++) {
|
||||
@@ -45,6 +45,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
</script>
|
||||
{% endblock %}
|
||||
</script>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user