Writer and manager updates.

This commit is contained in:
lwark
2025-07-22 13:15:24 -05:00
parent 53c6668ce1
commit b9ca9586ec
21 changed files with 661 additions and 329 deletions

View File

@@ -1363,6 +1363,7 @@ class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True)
name = Column(String, unique=True)
repeat = Column(INTEGER, nullable=False)
repeat_of = Column(String)
started_date = Column(TIMESTAMP)
completed_date = Column(TIMESTAMP)
@@ -1416,6 +1417,8 @@ class Procedure(BaseClass):
tips = association_proxy("proceduretipsassociation",
"tips")
@validates('repeat')
def validate_repeat(self, key, value):
if value > 1:
@@ -1461,9 +1464,16 @@ class Procedure(BaseClass):
def add_results(self, obj, resultstype_name: str):
logger.debug(f"Add Results! {resultstype_name}")
from ...managers import results
results_class = getattr(results, resultstype_name)
rs = results_class(procedure=self, parent=obj)
from backend.managers import results
results_manager = getattr(results, f"{resultstype_name}Manager")
rs = results_manager(procedure=self, parent=obj)
procedure = rs.procedure_to_pydantic()
samples = rs.samples_to_pydantic()
procedure_sql = procedure.to_sql()
procedure_sql.save()
for sample in samples:
sample_sql = sample.to_sql()
sample_sql.save()
def add_equipment(self, obj):
"""
@@ -1549,7 +1559,14 @@ class Procedure(BaseClass):
from backend.validators.pydant import PydResults, PydReagent
output = super().to_pydantic()
logger.debug(f"Pydantic output: \n\n{pformat(output.__dict__)}\n\n")
try:
output.kittype = dict(value=output.kittype['name'], missing=False)
except KeyError:
try:
output.kittype = dict(value=output.kittype['value'], missing=False)
except KeyError as e:
logger.error(f"Output.kittype: {output.kittype}")
raise e
output.sample = [item.to_pydantic() for item in output.proceduresampleassociation]
reagents = []
for reagent in output.reagent:
@@ -1578,6 +1595,10 @@ class Procedure(BaseClass):
# sample.enabled = True
return output
def create_proceduresampleassociations(self, sample):
from backend.db.models import ProcedureSampleAssociation
return ProcedureSampleAssociation(procedure=self, sample=sample)
class ProcedureTypeKitTypeAssociation(BaseClass):
"""
@@ -1967,7 +1988,10 @@ class ProcedureReagentAssociation(BaseClass):
try:
return f"<ProcedureReagentAssociation({self.procedure.procedure.rsl_plate_number} & {self.reagent.lot})>"
except AttributeError:
try:
logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!")
except AttributeError:
return "<ProcedureReagentAssociation(Unknown Submission & Unknown Reagent)>"
return f"<ProcedureReagentAssociation(Unknown Submission & {self.reagent.lot})>"
def __init__(self, reagent=None, procedure=None, reagentrole=""):
@@ -3093,12 +3117,12 @@ class Results(BaseClass):
_img = Column(String(128))
@property
def image(self) -> bytes:
def image(self) -> bytes|None:
dir = self.__directory_path__.joinpath("submission_imgs.zip")
try:
assert dir.exists()
except AssertionError:
raise FileNotFoundError(f"{dir} not found.")
return None
logger.debug(f"Getting image from {self.__directory_path__}")
with zipfile.ZipFile(dir) as zf:
with zf.open(self._img) as f:

View File

@@ -671,24 +671,18 @@ class Run(BaseClass, LogMixin):
def sample_count(self):
return len(self.sample)
def details_dict(self, **kwargs):
output = super().details_dict()
output['plate_number'] = self.plate_number
submission_samples = [sample for sample in self.clientsubmission.sample]
# logger.debug(f"Submission samples:{pformat(submission_samples)}")
active_samples = [sample.details_dict() for sample in output['runsampleassociation']
if sample.sample.sample_id in [s.sample_id for s in submission_samples]]
# logger.debug(f"Active samples:{pformat(active_samples)}")
for sample in active_samples:
sample['active'] = True
inactive_samples = [sample.details_dict() for sample in submission_samples if
sample.name not in [s['sample_id'] for s in active_samples]]
# logger.debug(f"Inactive samples:{pformat(inactive_samples)}")
for sample in inactive_samples:
sample['active'] = False
# output['sample'] = [sample.details_dict() for sample in output['runsampleassociation']]
output['sample'] = active_samples + inactive_samples
output['procedure'] = [procedure.details_dict() for procedure in output['procedure']]
output['permission'] = is_power_user()
@@ -983,7 +977,7 @@ class Run(BaseClass, LogMixin):
new_dict['name'] = field_value
case "id":
continue
case "clientsubmission":
case "clientsubmission" | "client_submission":
field_value = self.clientsubmission.to_pydantic()
case "procedure":
field_value = [item.to_pydantic() for item in self.procedure]
@@ -1243,8 +1237,20 @@ class Run(BaseClass, LogMixin):
logger.debug(f"Got ProcedureType: {procedure_type}")
dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self))
if dlg.exec():
sql, _ = dlg.return_sql()
logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}")
sql, _ = dlg.return_sql(new=True)
# logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}")
# previous = [proc for proc in self.procedure if proc.proceduretype == procedure_type]
# repeats = len([proc for proc in previous if proc.repeat])
# if sql.repeat:
# repeats += 1
# if repeats > 0:
# suffix = f"-{str(len(previous))}R{repeats}"
# else:
# suffix = f"-{str(len(previous)+1)}"
# sql.name = f"{sql.repeat}{suffix}"
# else:
# suffix = f"-{str(len(previous)+1)}"
# sql.name = f"{self.name}-{proceduretype_name}{suffix}"
sql.save()
obj.set_data()

View File

@@ -4,13 +4,17 @@
import logging
from backend.db.models import Run, Sample, Procedure, ProcedureSampleAssociation
from backend.excel.parsers import DefaultKEYVALUEParser, DefaultTABLEParser
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
# class PCRResultsParser(DefaultParser):
# pass
class PCRInfoParser(DefaultKEYVALUEParser):
pyd_name = "PydResults"
default_range_dict = [dict(
start_row=1,
end_row=24,
@@ -19,65 +23,38 @@ class PCRInfoParser(DefaultKEYVALUEParser):
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 __init__(self, filepath: Path | str, range_dict: dict | None = None, procedure=None):
super().__init__(filepath=filepath, range_dict=range_dict)
self.procedure = procedure
def to_pydantic(self):
# from backend.db.models import Procedure
data = {key: value for key, value in self.parsed_info}
data['filepath'] = self.filepath
data = dict(results={key: value for key, value in self.parsed_info}, filepath=self.filepath,
result_type="PCR")
return self._pyd_object(**data, parent=self.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
class PCRSampleParser(DefaultTABLEParser):
"""Object to pull data from Design and Analysis PCR export file."""
pyd_name = "PydResults"
default_range_dict = [dict(
header_row=25,
sheet="Results"
)]
def __init__(self, filepath: Path | str, range_dict: dict | None = None, procedure=None):
super().__init__(filepath=filepath, range_dict=range_dict)
self.procedure = procedure
@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()
multi = dict(result_type="PCR")
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" and k != "sample"}
@@ -86,11 +63,18 @@ class PCRSampleParser(DefaultTABLEParser):
def to_pydantic(self):
logger.debug(f"running to pydantic")
for item in self.parsed_info:
sample_obj = Sample.query(sample_id=list(item.keys())[0])
# sample_obj = Sample.query(sample_id=list(item.keys())[0])
# NOTE: Ensure that only samples associated with the procedure are used.
try:
sample_obj = next(
(sample for sample in self.procedure.sample if sample.sample_id == list(item.keys())[0]))
except StopIteration:
continue
logger.debug(f"Sample object {sample_obj}")
assoc = ProcedureSampleAssociation.query(sample=sample_obj, procedure=self.procedure)
if assoc and not isinstance(assoc, list):
yield self._pyd_object(results=list(item.values())[0], parent=assoc)
output = self._pyd_object(results=list(item.values())[0], parent=assoc)
output.result_type = "PCR"
yield output
else:
continue

View File

@@ -0,0 +1,175 @@
import logging
import re
from io import BytesIO
from pathlib import Path
from pprint import pformat
from typing import Any
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
from pandas import DataFrame
from backend.db.models import BaseClass
from backend.validators.pydant import PydBaseClass
logger = logging.getLogger(f"submissions.{__name__}")
class DefaultWriter(object):
def __repr__(self):
return f"{self.__class__.__name__}<{self.filepath.stem}>"
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
# self.filepath = output_filepath
self.pydant_obj = pydant_obj
self.fill_dictionary = pydant_obj.improved_dict()
if range_dict:
self.range_dict = range_dict
else:
self.range_dict = self.__class__.default_range_dict
@classmethod
def stringify_value(cls, value:Any) -> str:
match value:
case x if issubclass(value.__class__, BaseClass):
value = value.name
case x if issubclass(value.__class__, PydBaseClass):
value = value.name
case dict():
try:
value = value['value']
except ValueError:
try:
value = value['name']
except ValueError:
value = value.__str__()
case _:
value = str(value)
return value
@classmethod
def prettify_key(cls, value:str) -> str:
value = value.replace("type", " type").strip()
value = value.title()
return value
def write_to_workbook(self, workbook: Workbook):
logger.debug(f"Writing to workbook with {self.__class__.__name__}")
return workbook
class DefaultKEYVALUEWriter(DefaultWriter):
default_range_dict = [dict(
start_row=2,
end_row=18,
key_column=1,
value_column=2,
sheet="Sample List"
)]
@classmethod
def check_location(cls, locations: list, sheet: str):
return any([item['sheet'] == sheet for item in locations])
def write_to_workbook(self, workbook: Workbook) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook)
for rng in self.range_dict:
rows = range(rng['start_row'], rng['end_row'] + 1)
worksheet = workbook[rng['sheet']]
try:
for ii, (k, v) in enumerate(self.fill_dictionary.items(), start=rng['start_row']):
# match v:
# case x if issubclass(v.__class__, BaseClass):
# v = v.name
# case x if issubclass(v.__class__, PydBaseClass):
# v = v.name
# case dict():
# try:
# v = v['value']
# except ValueError:
# try:
# v = v['name']
# except ValueError:
# v = v.__str__()
# case _:
# pass
try:
worksheet.cell(column=rng['key_column'], row=rows[ii], value=self.prettify_key(k))
worksheet.cell(column=rng['value_column'], row=rows[ii], value=self.stringify_value(v))
except IndexError:
logger.error(f"Not enough rows: {len(rows)} for index {ii}")
except ValueError as e:
logger.error(self.fill_dictionary)
raise e
return workbook
class DefaultTABLEWriter(DefaultWriter):
default_range_dict = [dict(
header_row=19,
sheet="Sample List"
)]
@classmethod
def get_row_count(cls, worksheet: Worksheet, range_dict:dict):
if "end_row" in range_dict.keys():
list_df = DataFrame([item for item in worksheet.values][range_dict['header_row'] - 1:range_dict['end_row'] - 1])
else:
list_df = DataFrame([item for item in worksheet.values][range_dict['header_row'] - 1:])
row_count = list_df.shape[0]
return row_count
def pad_samples_to_length(self, row_count, column_names):
from backend import PydSample
output_samples = []
for iii in range(1, row_count + 1):
logger.debug(f"Submission rank: {iii}")
if isinstance(self.pydant_obj, list):
iterator = self.pydant_obj
else:
iterator = self.pydant_obj.sample
try:
sample = next((item for item in iterator if item.submission_rank == iii))
except StopIteration:
sample = PydSample(sample_id="")
for column in column_names:
setattr(sample, column[0], "")
sample.submission_rank = iii
logger.debug(f"Appending {sample.sample_id}")
logger.debug(f"Iterator now: {[item.submission_rank for item in iterator]}")
output_samples.append(sample)
return sorted(output_samples, key=lambda x: x.submission_rank)
def write_to_workbook(self, workbook: Workbook) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook)
for rng in self.range_dict:
list_worksheet = workbook[rng['sheet']]
column_names = [(item.value.lower().replace(" ", "_"), item.column) for item in list_worksheet[rng['header_row']] if item.value]
for iii, object in enumerate(self.pydant_obj, start=1):
# logger.debug(f"Writing object: {object}")
write_row = rng['header_row'] + iii
for column in column_names:
if column[0].lower() in ["well", "row", "column"]:
continue
write_column = column[1]
try:
value = getattr(object, column[0].lower().replace(" ", ""))
except AttributeError:
try:
value = getattr(object, column[0].lower().replace("_", ""))
except AttributeError:
value = ""
# logger.debug(f"{column} Writing {value} to row {write_row}, column {write_column}")
list_worksheet.cell(row=write_row, column=write_column, value=self.stringify_value(value))
return workbook
from .clientsubmission_writer import ClientSubmissionInfoWriter, ClientSubmissionSampleWriter

View File

@@ -0,0 +1,73 @@
import logging
from pathlib import Path
from pprint import pformat
from openpyxl.workbook import Workbook
from . import DefaultKEYVALUEWriter, DefaultTABLEWriter
logger = logging.getLogger(f"submissions.{__name__}")
class ClientSubmissionInfoWriter(DefaultKEYVALUEWriter):
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
logger.debug(f"{self.__class__.__name__} recruited!")
def write_to_workbook(self, workbook: Workbook) -> Workbook:
# workbook = super().write_to_workbook(workbook=workbook)
logger.debug(f"Skipped super.")
for rng in self.range_dict:
worksheet = workbook[rng['sheet']]
for key, value in self.fill_dictionary.items():
logger.debug(f"Checking: key {key}, value {str(value)[:64]}")
if isinstance(value, bytes):
continue
try:
check = self.check_location(value['location'], rng['sheet'])
except TypeError:
check = False
if not check:
continue
# relevant_values[k] = v
logger.debug(f"Location passed for {value['location']}")
for location in value['location']:
if location['sheet'] != rng['sheet']:
continue
logger.debug(f"Writing {value} to row {location['row']}, column {location['value_column']}")
try:
worksheet.cell(location['row'], location['value_column'], value=value['value'])
except KeyError:
worksheet.cell(location['row'], location['value_column'], value=value['name'])
return workbook
class ClientSubmissionSampleWriter(DefaultTABLEWriter):
def write_to_workbook(self, workbook: Workbook) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook)
for rng in self.range_dict:
list_worksheet = workbook[rng['sheet']]
row_count = self.get_row_count(list_worksheet, rng)
column_names = [(item.value.lower().replace(" ", "_"), item.column) for item in list_worksheet[rng['header_row']] if item.value]
samples = self.pad_samples_to_length(row_count=row_count, column_names=column_names)
for sample in samples:
# logger.debug(f"Writing sample: {sample}")
write_row = rng['header_row'] + sample.submission_rank
for column in column_names:
if column[0].lower() in ["well", "row", "column"]:
continue
write_column = column[1]
try:
# value = sample[column[0]]
value = getattr(sample, column[0])
except AttributeError:
value = ""
# logger.debug(f"{column} Writing {value} to row {write_row}, column {write_column}")
list_worksheet.cell(row=write_row, column=write_column, value=value)
return workbook

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
import logging
from pprint import pformat
from openpyxl.workbook import Workbook
from backend.excel.writers import DefaultKEYVALUEWriter, DefaultTABLEWriter
logger = logging.getLogger(f"submissions.{__name__}")
class ProcedureInfoWriter(DefaultKEYVALUEWriter):
default_range_dict = [dict(
start_row=1,
end_row=6,
key_column=1,
value_column=2,
sheet=""
)]
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
exclude = ['control', 'equipment', 'excluded', 'id', 'misc_info', 'plate_map', 'possible_kits', 'procedureequipmentassociation',
'procedurereagentassociation', 'proceduresampleassociation', 'proceduretipsassociation', 'reagent', 'reagentrole',
'results', 'sample', 'tips']
self.fill_dictionary = {k: v for k, v in self.fill_dictionary.items() if k not in exclude}
logger.debug(pformat(self.fill_dictionary))
for rng in self.range_dict:
if "sheet" not in rng or rng['sheet'] == "":
rng['sheet'] = f"{pydant_obj.proceduretype.name} Quality"
class ProcedureReagentWriter(DefaultTABLEWriter):
default_range_dict = [dict(
header_row=8
)]
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
for rng in self.range_dict:
if "sheet" not in rng:
rng['sheet'] = f"{pydant_obj.proceduretype.name} Quality"
self.pydant_obj = self.pydant_obj.reagent
class ProcedureEquipmentWriter(DefaultTABLEWriter):
default_range_dict = [dict(
header_row=14
)]
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
for rng in self.range_dict:
if "sheet" not in rng:
rng['sheet'] = f"{pydant_obj.proceduretype.name} Quality"
self.pydant_obj = self.pydant_obj.equipment
class ProcedureSampleWriter(DefaultTABLEWriter):
default_range_dict = [dict(
header_row=21
)]
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
for rng in self.range_dict:
if "sheet" not in rng:
rng['sheet'] = f"{pydant_obj.proceduretype.name} Quality"
self.pydant_obj = self.pydant_obj.sample
def write_to_workbook(self, workbook: Workbook) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook)
for rng in self.range_dict:
list_worksheet = workbook[rng['sheet']]
row_count = self.get_row_count(list_worksheet, rng)
column_names = [(item.value.lower().replace(" ", "_"), item.column) for item in
list_worksheet[rng['header_row']] if item.value]
samples = self.pad_samples_to_length(row_count=row_count, column_names=column_names)
# samples = self.pydant_obj
logger.debug(f"Samples: {[item.submission_rank for item in samples]}")
for sample in samples:
logger.debug(f"Writing sample: {sample}")
write_row = rng['header_row'] + sample.submission_rank
for column in column_names:
if column[0].lower() in ["well"]:#, "row", "column"]:
continue
write_column = column[1]
try:
value = getattr(sample, column[0])
except KeyError:
value = ""
logger.debug(f"{column} Writing {value} to row {write_row}, column {write_column}")
list_worksheet.cell(row=write_row, column=write_column, value=value)
return workbook

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import logging
from io import BytesIO
from typing import TYPE_CHECKING
from pathlib import Path
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook
from backend.validators import RSLNamer
from backend.managers import DefaultManager
from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser
from backend.excel.writers.clientsubmission_writer import ClientSubmissionInfoWriter, ClientSubmissionSampleWriter
if TYPE_CHECKING:
from backend.db.models import SubmissionType
logger = logging.getLogger(f"submissions.{__name__}")
class DefaultClientSubmissionManager(DefaultManager):
def __init__(self, parent, submissiontype: "SubmissionType" | str | None = None,
input_object: Path | str | None = None):
from backend.db.models import SubmissionType
match input_object:
case str() | Path():
submissiontype = RSLNamer.retrieve_submission_type(input_object)
case _:
logger.warning(f"Skipping submission type")
match submissiontype:
case str():
submissiontype = SubmissionType.query(name=submissiontype)
case dict():
submissiontype = SubmissionType.query(name=submissiontype['name'])
case SubmissionType():
pass
case _:
raise TypeError(f"Unknown type for submissiontype of {type(submissiontype)}")
self.submissiontype = submissiontype
super().__init__(parent=parent, input_object=input_object)
def parse(self):
self.info_parser = ClientSubmissionInfoParser(filepath=self.input_object, submissiontype=self.submissiontype)
self.sample_parser = ClientSubmissionSampleParser(filepath=self.input_object,
submissiontype=self.submissiontype)
self.to_pydantic()
return self.clientsubmission
def to_pydantic(self):
self.clientsubmission = self.info_parser.to_pydantic()
self.clientsubmission.sample = self.sample_parser.to_pydantic()
def write(self):
workbook: Workbook = load_workbook(BytesIO(self.submissiontype.template_file))
self.info_writer = ClientSubmissionInfoWriter(pydant_obj=self.pyd)
assert isinstance(self.info_writer, ClientSubmissionInfoWriter)
logger.debug("Attempting write.")
workbook = self.info_writer.write_to_workbook(workbook)
self.sample_writer = ClientSubmissionSampleWriter(pydant_obj=self.pyd)
workbook = self.sample_writer.write_to_workbook(workbook)
# workbook.save(output_path)
return workbook

View File

@@ -1,9 +1,16 @@
from __future__ import annotations
import logging
from .. import DefaultManager
from backend.db.models import Procedure
from pathlib import Path
from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from backend.validators.pydant import PydResults
logger = logging.getLogger(f"submission.{__name__}")
@@ -18,4 +25,13 @@ class DefaultResultsManager(DefaultManager):
self.fname = Path(fname)
logger.debug(f"FName after correction: {fname}")
def procedure_to_pydantic(self) -> PydResults:
info = self.info_parser.to_pydantic()
info.parent = self.procedure
return info
def samples_to_pydantic(self) -> List[PydResults]:
sample = [item for item in self.sample_parser.to_pydantic()]
return sample
from .pcr_results_manager import PCRManager

View File

@@ -1,32 +1,29 @@
"""
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Tuple, List, TYPE_CHECKING
from backend.db.models import Procedure
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser
from . import DefaultResultsManager
if TYPE_CHECKING:
from backend.validators.pydant import PydResults
logger = logging.getLogger(f"submissions.{__name__}")
class PCRManager(DefaultResultsManager):
def __init__(self, procedure: Procedure, parent, fname:Path|str|None=None):
def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None):
super().__init__(procedure=procedure, parent=parent, fname=fname)
self.parse()
def parse(self):
self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure)
self.sample_parser = PCRSampleParser(filepath=self.fname, procedure=self.procedure)
self.build_info()
self.build_samples()
def build_info(self):
procedure_info = self.info_parser.to_pydantic()
procedure_info.results_type = self.__class__.__name__
procedure_sql = procedure_info.to_sql()
procedure_sql.save()
def build_samples(self):
samples = self.sample_parser.to_pydantic()
for sample in samples:
sample.results_type = self.__class__.__name__
sql = sample.to_sql()
sql.save()

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
import logging
from pathlib import Path
from openpyxl import load_workbook
from openpyxl.workbook.workbook import Workbook
from tools import copy_xl_sheet
from backend.managers import DefaultManager
logger = logging.getLogger(f"submissions.{__name__}")
class DefaultRunManager(DefaultManager):
def write(self) -> Workbook:
from backend.managers import DefaultClientSubmissionManager, DefaultProcedureManager
logger.debug(f"Initializing write")
clientsubmission = DefaultClientSubmissionManager(parent=self.parent, input_object=self.pyd.clientsubmission, submissiontype=self.pyd.clientsubmission.submissiontype)
workbook = clientsubmission.write()
for procedure in self.pyd.procedure:
logger.debug(f"Running procedure: {procedure}")
procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype.name, parent=self.parent, input_object=procedure)
wb: Workbook = procedure.write()
for sheetname in wb.sheetnames:
source_sheet = wb[sheetname]
ws = workbook.create_sheet(sheetname)
copy_xl_sheet(source_sheet, ws)
return workbook

View File

@@ -516,44 +516,6 @@ class PydRun(PydBaseClass, extra='allow'):
value = dict(value=value, missing=True)
return value
# @field_validator("tips", mode="before")
# @classmethod
# def expand_tips(cls, value):
# if isinstance(value, dict):
# value = value['value']
# if isinstance(value, Generator):
# return [PydTips(**tips) for tips in value]
# if not value:
# return []
# return value
#
# @field_validator('equipment', mode='before')
# @classmethod
# def convert_equipment_dict(cls, value):
# if isinstance(value, dict):
# return value['value']
# if isinstance(value, Generator):
# return [PydEquipment(**equipment) for equipment in value]
# if not value:
# return []
# return value
# @field_validator('comment', mode='before')
# @classmethod
# def create_comment(cls, value):
# if value is None:
# return ""
# return value
#
# @field_validator("submitter_plate_id")
# @classmethod
# def enforce_with_uuid(cls, value):
# if value['value'] in [None, "None"]:
# return dict(value=uuid.uuid4().hex.upper(), missing=True)
# else:
# value['value'] = value['value'].strip()
# return value
@field_validator("run_cost")
@classmethod
def rescue_run_cost(cls, value):
@@ -635,36 +597,6 @@ class PydRun(PydBaseClass, extra='allow'):
value['value'] = output.replace(tzinfo=timezone)
return value
# @field_validator("clientlab", mode="before")
# @classmethod
# def rescue_submitting_lab(cls, value):
# if value is None:
# return dict(value=None, missing=True)
# return value
#
# @field_validator("clientlab")
# @classmethod
# def lookup_submitting_lab(cls, value):
# if isinstance(value['value'], str):
# try:
# value['value'] = ClientLab.query(name=value['value']).name
# except AttributeError:
# value['value'] = None
# if value['value'] is None:
# value['missing'] = True
# if "pytest" in sys.modules:
# value['value'] = "Nosocomial"
# return value
# from frontend.widgets.pop_ups import ObjectSelector
# dlg = ObjectSelector(title="Missing Submitting Lab",
# message="We need a submitting lab. Please select from the list.",
# obj_type=ClientLab)
# if dlg.exec():
# value['value'] = dlg.parse_form()
# else:
# value['value'] = None
# return value
@field_validator("rsl_plate_number", mode='before')
@classmethod
def rescue_rsl_number(cls, value):
@@ -686,26 +618,8 @@ class PydRun(PydBaseClass, extra='allow'):
# try:
output = RSLNamer(filename=sub_type.filepath.__str__(), submission_type=sub_type.submissiontype,
data=values.data).parsed_name
return dict(value=output, missing=True)
# @field_validator("technician", mode="before")
# @classmethod
# def rescue_tech(cls, value):
# if value is None:
# return dict(value=None, missing=True)
# return value
#
# @field_validator("technician")
# @classmethod
# def enforce_tech(cls, value):
# if check_not_nan(value['value']):
# value['value'] = re.sub(r"\: \d", "", value['value'])
# return value
# else:
# return dict(value=convert_nans_to_nones(value['value']), missing=True)
@field_validator("sample_count", mode='before')
@classmethod
def rescue_sample_count(cls, value):
@@ -713,55 +627,6 @@ class PydRun(PydBaseClass, extra='allow'):
return dict(value=None, missing=True)
return value
# @field_validator("kittype", mode='before')
# @classmethod
# def rescue_kit(cls, value):
# if check_not_nan(value):
# if isinstance(value, str):
# return dict(value=value, missing=False)
# elif isinstance(value, dict):
# return value
# else:
# raise ValueError(f"No extraction kittype found.")
# if value is None:
# # NOTE: Kit selection is done in the clientsubmissionparser, so should not be necessary here.
# return dict(value=None, missing=True)
# return value
#
# @field_validator("submissiontype", mode='before')
# @classmethod
# def make_submission_type(cls, value, values):
# if not isinstance(value, dict):
# value = dict(value=value)
# if check_not_nan(value['value']):
# value = value['value'].title()
# return dict(value=value, missing=False)
# else:
# return dict(value=RSLNamer.retrieve_submission_type(filename=values.data['filepath']).title(), missing=True)
#
# @field_validator("submission_category", mode="before")
# @classmethod
# def create_category(cls, value):
# if not isinstance(value, dict):
# return dict(value=value, missing=True)
# return value
#
# @field_validator("submission_category")
# @classmethod
# def rescue_category(cls, value, values):
# if isinstance(value['value'], str):
# value['value'] = value['value'].title()
# if value['value'] not in ["Research", "Diagnostic", "Surveillance", "Validation"]:
# value['value'] = values.data['proceduretype']['value']
# return value
# @field_validator("reagent", mode="before")
# @classmethod
# def expand_reagents(cls, value):
# if isinstance(value, Generator):
# return [PydReagent(**reagent) for reagent in value]
# return value
@field_validator("sample", mode="before")
@classmethod
def expand_samples(cls, value):
@@ -769,77 +634,9 @@ class PydRun(PydBaseClass, extra='allow'):
return [PydSample(**sample) for sample in value]
return value
# @field_validator("sample")
# @classmethod
# def assign_ids(cls, value):
# starting_id = ClientSubmissionSampleAssociation.autoincrement_id()
# for iii, sample in enumerate(value, start=starting_id):
# # NOTE: Why is this a list? Answer: to zip with the lists of rows and columns in case of multiple of the same sample.
# sample.assoc_id = [iii]
# return value
# @field_validator("cost_centre", mode="before")
# @classmethod
# def rescue_cost_centre(cls, value):
# match value:
# case dict():
# return value
# case _:
# return dict(value=value, missing=True)
#
# @field_validator("cost_centre")
# @classmethod
# def get_cost_centre(cls, value, values):
# match value['value']:
# case None:
# from backend.db.models import Organization
# org = Organization.query(name=values.data['clientlab']['value'])
# try:
# return dict(value=org.cost_centre, missing=True)
# except AttributeError:
# return dict(value="xxx", missing=True)
# case _:
# return value
#
# @field_validator("contact")
# @classmethod
# def get_contact_from_org(cls, value, values):
# # logger.debug(f"Value coming in: {value}")
# match value:
# case dict():
# if isinstance(value['value'], tuple):
# value['value'] = value['value'][0]
# case tuple():
# value = dict(value=value[0], missing=False)
# case _:
# value = dict(value=value, missing=False)
# # logger.debug(f"Value after match: {value}")
# check = Contact.query(name=value['value'])
# # logger.debug(f"Check came back with {check}")
# if not isinstance(check, Contact):
# org = values.data['clientlab']['value']
# # logger.debug(f"Checking organization: {org}")
# if isinstance(org, str):
# org = ClientLab.query(name=values.data['clientlab']['value'], limit=1)
# if isinstance(org, ClientLab):
# contact = org.contact[0].name
# else:
# logger.warning(f"All attempts at defaulting Contact failed, returning: {value}")
# return value
# if isinstance(contact, tuple):
# contact = contact[0]
# value = dict(value=f"Defaulted to: {contact}", missing=False)
# # logger.debug(f"Value after query: {value}")
# return value
# else:
# # logger.debug(f"Value after bypass check: {value}")
# return value
def __init__(self, run_custom: bool = False, **data):
super().__init__(**data)
# NOTE: this could also be done with default_factory
# self.submission_object = Run.find_polymorphic_subclass(
# polymorphic_identity=self.submission_type['value'])
submission_type = self.clientsubmission.submissiontype
# logger.debug(submission_type)
self.namer = RSLNamer(self.rsl_plate_number['value'], submission_type=submission_type)
@@ -1504,6 +1301,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
name: dict = Field(default=dict(value="NA", missing=True), validate_default=True)
technician: dict = Field(default=dict(value="NA", missing=True))
repeat: bool = Field(default=False)
repeat_of: str | None = Field(default=None)
kittype: dict = Field(default=dict(value="NA", missing=True))
possible_kits: list | None = Field(default=[], validate_default=True)
plate_map: str | None = Field(default=None)
@@ -1516,6 +1314,8 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
@field_validator("name", "technician", "kittype", mode="before")
@classmethod
def convert_to_dict(cls, value):
if not value:
value = "NA"
if isinstance(value, str):
value = dict(value=value, missing=False)
return value
@@ -1597,6 +1397,13 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
value = Run.query(name=value)
return value
@field_validator("repeat_of")
@classmethod
def drop_empty_string(cls, value):
if value == "":
value = None
return value
def update_kittype_reagentroles(self, kittype: str | KitType):
if kittype == self.__class__.model_fields['kittype'].default['value']:
return
@@ -1687,14 +1494,21 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
reg = reagent.to_sql()
reg.save()
def to_sql(self):
def to_sql(self, new: bool=False):
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
# results = []
# for result in self.results:
# result, _ = result.to_sql()
if new:
sql = Procedure()
else:
sql = super().to_sql()
# logger.debug(f"Initial PYD: {pformat(self.__dict__)}")
logger.debug(f"Initial PYD: {pformat(self.__dict__)}")
# sql.results = [result.to_sql() for result in self.results]
sql.repeat = self.repeat
if sql.repeat:
regex = re.compile(r".*\dR\d$")
repeats = [item for item in self.run.procedure if self.repeat_of in item.name and bool(regex.match(item.name))]
sql.name = f"{self.repeat_of}R{str(len(repeats)+1)}"
sql.repeat_of = self.repeat_of
sql.started_date = datetime.now()
if self.run:
sql.run = self.run
if self.proceduretype:
@@ -1710,13 +1524,16 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
for reagent in self.reagent:
if isinstance(reagent, dict):
reagent = PydReagent(**reagent)
logger.debug(reagent)
# logger.debug(reagent)
reagentrole = reagent.reagentrole
reagent = reagent.to_sql()
logger.debug(reagentrole)
# logger.debug(reagentrole)
if reagent not in sql.reagent:
# NOTE: Remove any previous association for this role.
if sql.id:
removable = ProcedureReagentAssociation.query(procedure=sql, reagentrole=reagentrole)
else:
removable = []
logger.debug(f"Removable: {removable}")
if removable:
if isinstance(removable, list):
@@ -1724,7 +1541,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
r.delete()
else:
removable.delete()
logger.debug(f"Adding {reagent} to {sql}")
# logger.debug(f"Adding {reagent} to {sql}")
reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql, reagentrole=reagentrole)
try:
start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1
@@ -1732,9 +1549,9 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
start_index = 1
relevant_samples = [sample for sample in self.sample if
not sample.sample_id.startswith("blank_") and not sample.sample_id == ""]
logger.debug(f"start index: {start_index}")
# logger.debug(f"start index: {start_index}")
assoc_id_range = range(start_index, start_index + len(relevant_samples) + 1)
logger.debug(f"Association id range: {assoc_id_range}")
# logger.debug(f"Association id range: {assoc_id_range}")
for iii, sample in enumerate(relevant_samples):
sample_sql = sample.to_sql()
if sql.run:
@@ -1751,11 +1568,6 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
kittype = KitType.query(name=self.kittype['value'], limit=1)
if kittype:
sql.kittype = kittype
# logger.debug(self.reagent)
# for reagent in self.reagent:
# reagent = reagent.to_sql()
# if reagent not in sql.reagent:
# reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql)
for equipment in self.equipment:
equip = Equipment.query(name=equipment.name)
if equip not in sql.equipment:
@@ -1859,7 +1671,7 @@ class PydClientSubmission(PydBaseClass):
if not value['value'] in ["Research", "Diagnostic", "Surveillance", "Validation"]:
try:
value['value'] = values.data['submissiontype']['value']
except AttributeError:
except (AttributeError, KeyError):
value['value'] = "NA"
return value
@@ -1938,7 +1750,7 @@ class PydClientSubmission(PydBaseClass):
class PydResults(PydBaseClass, arbitrary_types_allowed=True):
results: dict = Field(default={})
results_type: str = Field(default="NA")
result_type: str = Field(default="NA")
img: None | bytes = Field(default=None)
parent: Procedure | ProcedureSampleAssociation | None = Field(default=None)
date_analyzed: datetime | None = Field(default=None)
@@ -1956,7 +1768,7 @@ class PydResults(PydBaseClass, arbitrary_types_allowed=True):
return value
def to_sql(self):
sql, _ = Results.query_or_create(results_type=self.results_type, result=self.results)
sql, _ = Results.query_or_create(result_type=self.result_type, result=self.results)
try:
check = sql.image
except FileNotFoundError:

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import datetime
import os
import re
import sys, logging
from pathlib import Path
from pprint import pformat
@@ -81,6 +82,8 @@ class ProcedureCreation(QDialog):
equipmentrole['equipment'].index(item_in_er_list)))
proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True)
self.update_equipment = EquipmentUsage.update_equipment
regex = re.compile(r".*R\d$")
proceduretype_dict['previous'] = [""] + [item.name for item in self.run.procedure if item.proceduretype == self.proceduretype and not bool(regex.match(item.name))]
html = render_details_template(
template_name="procedure_creation",
# css_in=['new_context_menu'],
@@ -91,8 +94,8 @@ class ProcedureCreation(QDialog):
plate_map=self.plate_map,
edit=self.edit
)
with open("procedure_creation_rendered.html", "w") as f:
f.write(html)
# with open("procedure_creation_rendered.html", "w") as f:
# f.write(html)
self.webview.setHtml(html)
@pyqtSlot(str, str, str, str)
@@ -127,11 +130,18 @@ class ProcedureCreation(QDialog):
setattr(self.procedure.run, key, new_value)
case _:
attribute = getattr(self.procedure, key)
match attribute:
case dict():
attribute['value'] = new_value.strip('\"')
case _:
setattr(self.procedure, key, new_value.strip('\"'))
logger.debug(f"Set value for {key}: {getattr(self.procedure, key)}")
@pyqtSlot(str, bool)
def check_toggle(self, key: str, ischecked: bool):
# logger.debug(f"{key} is checked: {ischecked}")
logger.debug(f"{key} is checked: {ischecked}")
setattr(self.procedure, key, ischecked)
@pyqtSlot(str)
@@ -159,7 +169,7 @@ class ProcedureCreation(QDialog):
self.set_html()
@pyqtSlot(str, str)
def update_reagent(self, reagentrole:str, name_lot_expiry:str):
def update_reagent(self, reagentrole: str, name_lot_expiry: str):
try:
name, lot, expiry = name_lot_expiry.split(" - ")
except ValueError as e:
@@ -167,8 +177,8 @@ class ProcedureCreation(QDialog):
return
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
def return_sql(self):
return self.procedure.to_sql()
def return_sql(self, new: bool = False):
return self.procedure.to_sql(new=new)
# class ProcedureWebViewer(QWebEngineView):
#

View File

@@ -8,7 +8,7 @@ from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound
from backend.db.models import Run, Sample, Reagent, KitType, Equipment, Process, Tips
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent, list_str_comparator
from .functions import select_save_file, save_pdf
from pathlib import Path
import logging
@@ -50,14 +50,6 @@ class SubmissionDetails(QDialog):
# NOTE: setup channel
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
# match sub:
# case Run():
# self.run_details(run=sub)
# self.rsl_plate_number = sub.rsl_plate_number
# case Sample():
# self.sample_details(sample=sub)
# case Reagent():
# self.reagent_details(reagent=sub)
# NOTE: Used to maintain javascript functions.
self.object_details(object=sub)
self.webview.page().setWebChannel(self.channel)
@@ -75,8 +67,8 @@ class SubmissionDetails(QDialog):
self.webview.setHtml(html)
self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}")
with open(f"{object.__class__.__name__}_details_rendered.html", "w") as f:
f.write(html)
# f.write(html)
pass
def activate_export(self) -> None:
@@ -88,11 +80,11 @@ class SubmissionDetails(QDialog):
"""
title = self.webview.title()
self.setWindowTitle(title)
if "Submission" in title:
if list_str_comparator(title, ['ClientSubmission', "Run", "Procedure"], mode="starts_with"):
self.btn.setEnabled(True)
self.export_plate = title.split(" ")[-1]
else:
self.btn.setEnabled(False)
self.export_plate = title
try:
check = self.webview.history().items()[0].title()
except IndexError as e:

View File

@@ -11,27 +11,24 @@
<h2><u>Submission Details for {{ clientsubmission['name'] }}</u></h2>
{{ super() }}
<p>{% for key, value in clientsubmission.items() if key not in clientsubmission['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Id", "ID") }}: </b>{% if key=='cost' %}{% if clientsubmission['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Id", "ID") }}:</b> {% if key=='cost' %}{% if clientsubmission['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br/>
{% endfor %}
</p>
{% if clientsubmission['sample'] %}
<button type="button"><h3><u>Client Submitted Samples:</u></h3></button>
<!-- <div class="nested">-->
<p>{% for sample in clientsubmission['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="data-link sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id'] }}</a><br>
{% endfor %}</p>
<!-- </div>-->
{% endif %}
{% if clientsubmission['run'] %}
<button type="button"><h3><u>Runs:</u></h3></button>
<!-- <div class="nested">-->
<div class="nested">
{% for run in clientsubmission['run'] %}
{% with run=run, child=True %}
{% include "run_details.html" %}
{% endwith %}
{% endfor %}
<!-- </div>-->
</div>
{% endif %}
{% endblock %}
</body>

View File

@@ -150,9 +150,15 @@ ul.no-bullets {
.nested {
margin-left: 50px;
padding: 0 18px;
display: none;
overflow: hidden;
background-color: #f1f1f1;
.nested {
background-color: #ffffff;
}
}
.hidden_input {
display: none;
}
/* Style the button that is used to open and close the collapsible content */

View File

@@ -21,6 +21,24 @@ for(let i = 0; i < formtexts.length; i++) {
})
};
var repeat_box = document.getElementById("repeat");
repeat_box.addEventListener("input", function() {
backend.check_toggle("repeat", repeat_box.checked)
var repeat_str = document.getElementById("repeat_of");
if (repeat_box.checked) {
repeat_str.classList.remove("hidden_input");
} else {
repeat_str.classList.add("hidden_input");
}
})
var repeat_of = document.getElementById("repeat_of");
repeat_of.addEventListener("change", function() {
backend.text_changed("repeat_of", repeat_of.value)
})
var changed_it = new Event('change');
var reagentRoles = document.getElementsByClassName("reagentrole");

View File

@@ -23,7 +23,12 @@
<label for="technician">Technician:</label><br>
<input type="text" class="form_text full" id="technician" name="technician" width="100%" value="{{ procedure['technician']['value'] }}" background-color="{{ procedure['technician']['colour'] }}"><br><br>
<label for="repeat">Repeat:</label>
<input type="checkbox" class="form_check" id="repeat" name="repeat" value="{{ procedure['repeat'] }}"><br><br>
<input type="checkbox" id="repeat" name="repeat" value="{{ procedure['repeat'] }}"><br>
<select class="dropdown hidden_input" id="repeat_of">
{% for previous in proceduretype['previous'] %}
<option value="{{ previous }}">{{ previous }}</option>
{% endfor %}
</select><br><br>
<label>Kit Type:</label><br>
<select class="dropdown" id="kittype" background-colour="{{ procedure['kittype']['colour'] }}">
{% for kittype in procedure['possible_kits'] %}

View File

@@ -1,6 +1,5 @@
{% extends "details.html" %}
{% if not child %}
<head>
{% block head %}
{{ super() }}
@@ -17,7 +16,7 @@
{% endfor %}</p>
{% if procedure['reagent'] %}
<button type="button"><h3><u>Reagents:</u></h3></button>
&nbsp;&nbsp;&nbsp;&nbsp;<table style="border: 1px solid black; width: 100%; text-align: center">
<table style="border: 1px solid black; width: 100%; text-align: center">
<tr>
<th style="border: 1px solid black;">Reagent Role</th>
<th style="border: 1px solid black;">Reagent Name</th>
@@ -28,12 +27,12 @@
{% with reagent=reg, child=True %}
{% include "support/reagent_list.html" %}
{% endwith %}
{% endfor %}</p>
{% endfor %}
</table><br/>
{% endif %}
{% if procedure['equipment'] %}
<button type="button"><h3><u>Equipment:</u></h3></button>
&nbsp;&nbsp;&nbsp;&nbsp;<table style="border: 1px solid black; width: 100%; text-align: center">
<table style="border: 1px solid black; width: 100%; text-align: center">
<tr>
<th style="border: 1px solid black;">Equipment Role</th>
<th style="border: 1px solid black;">Equipment Name</th>
@@ -61,6 +60,7 @@
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p>
{% endif %}
{% endblock %}
{% if not child %}
</body>
{% endif %}

View File

@@ -1,4 +1,5 @@
{% extends "details.html" %}
{% if not child %}
<head>
{% block head %}
@@ -7,7 +8,7 @@
{% endblock %}
</head>
<body>
{% endif %}
{% block body %}
<h2><u>Run Details for {{ run['rsl_plate_number'] }}</u></h2>
{{ super() }}
@@ -22,11 +23,13 @@
{% endif %}
{% if run['procedure'] %}
<button type="button"><h3><u>Procedures:</u></h3></button>
<div class="nested">
{% for procedure in run['procedure'] %}
{% with procedure=procedure, child=True %}
{% include "procedure_details.html" %}
{% endwith %}
{% endfor %}
</div>
{% endif %}
{% endblock %}
{% block signing_button %}
@@ -34,7 +37,9 @@
{% endblock %}
<br>
{% if not child %}
</body>
{% endif %}
{% block script %}
{{ super() }}

View File

@@ -0,0 +1,10 @@
<tr>
<td style="border: 1px solid black;">{{ equipment['equipmentrole'] }}</td>
<td style="border: 1px solid black;">{{ equipment['name'] }}</td>
<td style="border: 1px solid black;">{{ equipment['process']['name'] }}</td>
{% if equipment['tips'] %}
<td style="border: 1px solid black;">{{ equipment['tips']['name'] }}</td>
{% else %}
<td style="border: 1px solid black;"></td>
{% endif %}
</tr>

View File

@@ -550,6 +550,21 @@ def copy_cells(source_sheet, target_sheet):
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment:
target_cell.comment = copy(source_cell.comment)
def list_str_comparator(input_str:str, listy: List[str], mode: Literal["starts_with", "contains"]) -> bool:
match mode:
case "starts_with":
if any([input_str.startswith(item) for item in listy]):
return True
else:
return False
case "contains":
if any([item in input_str for item in listy]):
return True
else:
return False
def setup_lookup(func):
"""
Checks to make sure all args are allowed