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

View File

@@ -671,24 +671,18 @@ class Run(BaseClass, LogMixin):
def sample_count(self): def sample_count(self):
return len(self.sample) return len(self.sample)
def details_dict(self, **kwargs): def details_dict(self, **kwargs):
output = super().details_dict() output = super().details_dict()
output['plate_number'] = self.plate_number output['plate_number'] = self.plate_number
submission_samples = [sample for sample in self.clientsubmission.sample] 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'] active_samples = [sample.details_dict() for sample in output['runsampleassociation']
if sample.sample.sample_id in [s.sample_id for s in submission_samples]] 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: for sample in active_samples:
sample['active'] = True sample['active'] = True
inactive_samples = [sample.details_dict() for sample in submission_samples if inactive_samples = [sample.details_dict() for sample in submission_samples if
sample.name not in [s['sample_id'] for s in active_samples]] 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: for sample in inactive_samples:
sample['active'] = False sample['active'] = False
# output['sample'] = [sample.details_dict() for sample in output['runsampleassociation']]
output['sample'] = active_samples + inactive_samples output['sample'] = active_samples + inactive_samples
output['procedure'] = [procedure.details_dict() for procedure in output['procedure']] output['procedure'] = [procedure.details_dict() for procedure in output['procedure']]
output['permission'] = is_power_user() output['permission'] = is_power_user()
@@ -983,7 +977,7 @@ class Run(BaseClass, LogMixin):
new_dict['name'] = field_value new_dict['name'] = field_value
case "id": case "id":
continue continue
case "clientsubmission": case "clientsubmission" | "client_submission":
field_value = self.clientsubmission.to_pydantic() field_value = self.clientsubmission.to_pydantic()
case "procedure": case "procedure":
field_value = [item.to_pydantic() for item in self.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}") logger.debug(f"Got ProcedureType: {procedure_type}")
dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self)) dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self))
if dlg.exec(): if dlg.exec():
sql, _ = dlg.return_sql() sql, _ = dlg.return_sql(new=True)
logger.debug(f"Output run samples:\n{pformat(sql.run.sample)}") # 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() sql.save()
obj.set_data() obj.set_data()

View File

@@ -4,13 +4,17 @@
import logging import logging
from backend.db.models import Run, Sample, Procedure, ProcedureSampleAssociation from backend.db.models import Run, Sample, Procedure, ProcedureSampleAssociation
from backend.excel.parsers import DefaultKEYVALUEParser, DefaultTABLEParser from backend.excel.parsers import DefaultKEYVALUEParser, DefaultTABLEParser
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# class PCRResultsParser(DefaultParser): # class PCRResultsParser(DefaultParser):
# pass # pass
class PCRInfoParser(DefaultKEYVALUEParser): class PCRInfoParser(DefaultKEYVALUEParser):
pyd_name = "PydResults"
default_range_dict = [dict( default_range_dict = [dict(
start_row=1, start_row=1,
end_row=24, end_row=24,
@@ -19,65 +23,38 @@ class PCRInfoParser(DefaultKEYVALUEParser):
sheet="Results" sheet="Results"
)] )]
# def __init__(self, filepath: Path | str, range_dict: dict | None = None): def __init__(self, filepath: Path | str, range_dict: dict | None = None, procedure=None):
# super().__init__(filepath=filepath, range_dict=range_dict) super().__init__(filepath=filepath, range_dict=range_dict)
# self.worksheet = self.workbook[self.range_dict['sheet']] self.procedure = procedure
# 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): def to_pydantic(self):
# from backend.db.models import Procedure # from backend.db.models import Procedure
data = {key: value for key, value in self.parsed_info} data = dict(results={key: value for key, value in self.parsed_info}, filepath=self.filepath,
data['filepath'] = self.filepath result_type="PCR")
return self._pyd_object(**data, parent=self.procedure) 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): class PCRSampleParser(DefaultTABLEParser):
"""Object to pull data from Design and Analysis PCR export file.""" """Object to pull data from Design and Analysis PCR export file."""
pyd_name = "PydResults"
default_range_dict = [dict( default_range_dict = [dict(
header_row=25, header_row=25,
sheet="Results" 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 @property
def parsed_info(self): def parsed_info(self):
output = [item for item in super().parsed_info] output = [item for item in super().parsed_info]
merge_column = "sample" merge_column = "sample"
sample_names = list(set([item['sample'] for item in output])) sample_names = list(set([item['sample'] for item in output]))
for sample in sample_names: for sample in sample_names:
multi = dict() multi = dict(result_type="PCR")
sois = [item for item in output if item['sample'] == sample] sois = [item for item in output if item['sample'] == sample]
for soi in sois: for soi in sois:
multi[soi['target']] = {k: v for k, v in soi.items() if k != "target" and k != "sample"} 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): def to_pydantic(self):
logger.debug(f"running to pydantic") logger.debug(f"running to pydantic")
for item in self.parsed_info: 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}") logger.debug(f"Sample object {sample_obj}")
assoc = ProcedureSampleAssociation.query(sample=sample_obj, procedure=self.procedure) assoc = ProcedureSampleAssociation.query(sample=sample_obj, procedure=self.procedure)
if assoc and not isinstance(assoc, list): 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: else:
continue 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 import logging
from .. import DefaultManager from .. import DefaultManager
from backend.db.models import Procedure from backend.db.models import Procedure
from pathlib import Path from pathlib import Path
from frontend.widgets.functions import select_open_file from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent 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__}") logger = logging.getLogger(f"submission.{__name__}")
@@ -18,4 +25,13 @@ class DefaultResultsManager(DefaultManager):
self.fname = Path(fname) self.fname = Path(fname)
logger.debug(f"FName after correction: {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 from .pcr_results_manager import PCRManager

View File

@@ -1,32 +1,29 @@
""" """
""" """
from __future__ import annotations
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Tuple, List, TYPE_CHECKING
from backend.db.models import Procedure from backend.db.models import Procedure
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser
from . import DefaultResultsManager from . import DefaultResultsManager
if TYPE_CHECKING:
from backend.validators.pydant import PydResults
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class PCRManager(DefaultResultsManager): 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) super().__init__(procedure=procedure, parent=parent, fname=fname)
self.parse()
def parse(self):
self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure) self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure)
self.sample_parser = PCRSampleParser(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) value = dict(value=value, missing=True)
return value 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") @field_validator("run_cost")
@classmethod @classmethod
def rescue_run_cost(cls, value): def rescue_run_cost(cls, value):
@@ -635,36 +597,6 @@ class PydRun(PydBaseClass, extra='allow'):
value['value'] = output.replace(tzinfo=timezone) value['value'] = output.replace(tzinfo=timezone)
return value 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') @field_validator("rsl_plate_number", mode='before')
@classmethod @classmethod
def rescue_rsl_number(cls, value): def rescue_rsl_number(cls, value):
@@ -686,26 +618,8 @@ class PydRun(PydBaseClass, extra='allow'):
# try: # try:
output = RSLNamer(filename=sub_type.filepath.__str__(), submission_type=sub_type.submissiontype, output = RSLNamer(filename=sub_type.filepath.__str__(), submission_type=sub_type.submissiontype,
data=values.data).parsed_name data=values.data).parsed_name
return dict(value=output, missing=True) 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') @field_validator("sample_count", mode='before')
@classmethod @classmethod
def rescue_sample_count(cls, value): def rescue_sample_count(cls, value):
@@ -713,55 +627,6 @@ class PydRun(PydBaseClass, extra='allow'):
return dict(value=None, missing=True) return dict(value=None, missing=True)
return value 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") @field_validator("sample", mode="before")
@classmethod @classmethod
def expand_samples(cls, value): def expand_samples(cls, value):
@@ -769,77 +634,9 @@ class PydRun(PydBaseClass, extra='allow'):
return [PydSample(**sample) for sample in value] return [PydSample(**sample) for sample in value]
return 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): def __init__(self, run_custom: bool = False, **data):
super().__init__(**data) super().__init__(**data)
# NOTE: this could also be done with default_factory # 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 submission_type = self.clientsubmission.submissiontype
# logger.debug(submission_type) # logger.debug(submission_type)
self.namer = RSLNamer(self.rsl_plate_number['value'], submission_type=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) name: dict = Field(default=dict(value="NA", missing=True), validate_default=True)
technician: dict = Field(default=dict(value="NA", missing=True)) technician: dict = Field(default=dict(value="NA", missing=True))
repeat: bool = Field(default=False) repeat: bool = Field(default=False)
repeat_of: str | None = Field(default=None)
kittype: dict = Field(default=dict(value="NA", missing=True)) kittype: dict = Field(default=dict(value="NA", missing=True))
possible_kits: list | None = Field(default=[], validate_default=True) possible_kits: list | None = Field(default=[], validate_default=True)
plate_map: str | None = Field(default=None) 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") @field_validator("name", "technician", "kittype", mode="before")
@classmethod @classmethod
def convert_to_dict(cls, value): def convert_to_dict(cls, value):
if not value:
value = "NA"
if isinstance(value, str): if isinstance(value, str):
value = dict(value=value, missing=False) value = dict(value=value, missing=False)
return value return value
@@ -1597,6 +1397,13 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
value = Run.query(name=value) value = Run.query(name=value)
return 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): def update_kittype_reagentroles(self, kittype: str | KitType):
if kittype == self.__class__.model_fields['kittype'].default['value']: if kittype == self.__class__.model_fields['kittype'].default['value']:
return return
@@ -1687,14 +1494,21 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
reg = reagent.to_sql() reg = reagent.to_sql()
reg.save() reg.save()
def to_sql(self): def to_sql(self, new: bool=False):
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
# results = [] if new:
# for result in self.results: sql = Procedure()
# result, _ = result.to_sql() else:
sql = super().to_sql() 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.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: if self.run:
sql.run = self.run sql.run = self.run
if self.proceduretype: if self.proceduretype:
@@ -1710,13 +1524,16 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
for reagent in self.reagent: for reagent in self.reagent:
if isinstance(reagent, dict): if isinstance(reagent, dict):
reagent = PydReagent(**reagent) reagent = PydReagent(**reagent)
logger.debug(reagent) # logger.debug(reagent)
reagentrole = reagent.reagentrole reagentrole = reagent.reagentrole
reagent = reagent.to_sql() reagent = reagent.to_sql()
logger.debug(reagentrole) # logger.debug(reagentrole)
if reagent not in sql.reagent: if reagent not in sql.reagent:
# NOTE: Remove any previous association for this role. # NOTE: Remove any previous association for this role.
removable = ProcedureReagentAssociation.query(procedure=sql, reagentrole=reagentrole) if sql.id:
removable = ProcedureReagentAssociation.query(procedure=sql, reagentrole=reagentrole)
else:
removable = []
logger.debug(f"Removable: {removable}") logger.debug(f"Removable: {removable}")
if removable: if removable:
if isinstance(removable, list): if isinstance(removable, list):
@@ -1724,7 +1541,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
r.delete() r.delete()
else: else:
removable.delete() 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) reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql, reagentrole=reagentrole)
try: try:
start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 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 start_index = 1
relevant_samples = [sample for sample in self.sample if relevant_samples = [sample for sample in self.sample if
not sample.sample_id.startswith("blank_") and not sample.sample_id == ""] 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) 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): for iii, sample in enumerate(relevant_samples):
sample_sql = sample.to_sql() sample_sql = sample.to_sql()
if sql.run: if sql.run:
@@ -1751,11 +1568,6 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
kittype = KitType.query(name=self.kittype['value'], limit=1) kittype = KitType.query(name=self.kittype['value'], limit=1)
if kittype: if kittype:
sql.kittype = 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: for equipment in self.equipment:
equip = Equipment.query(name=equipment.name) equip = Equipment.query(name=equipment.name)
if equip not in sql.equipment: if equip not in sql.equipment:
@@ -1859,7 +1671,7 @@ class PydClientSubmission(PydBaseClass):
if not value['value'] in ["Research", "Diagnostic", "Surveillance", "Validation"]: if not value['value'] in ["Research", "Diagnostic", "Surveillance", "Validation"]:
try: try:
value['value'] = values.data['submissiontype']['value'] value['value'] = values.data['submissiontype']['value']
except AttributeError: except (AttributeError, KeyError):
value['value'] = "NA" value['value'] = "NA"
return value return value
@@ -1938,7 +1750,7 @@ class PydClientSubmission(PydBaseClass):
class PydResults(PydBaseClass, arbitrary_types_allowed=True): class PydResults(PydBaseClass, arbitrary_types_allowed=True):
results: dict = Field(default={}) results: dict = Field(default={})
results_type: str = Field(default="NA") result_type: str = Field(default="NA")
img: None | bytes = Field(default=None) img: None | bytes = Field(default=None)
parent: Procedure | ProcedureSampleAssociation | None = Field(default=None) parent: Procedure | ProcedureSampleAssociation | None = Field(default=None)
date_analyzed: datetime | None = Field(default=None) date_analyzed: datetime | None = Field(default=None)
@@ -1956,7 +1768,7 @@ class PydResults(PydBaseClass, arbitrary_types_allowed=True):
return value return value
def to_sql(self): 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: try:
check = sql.image check = sql.image
except FileNotFoundError: except FileNotFoundError:

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import datetime import datetime
import os import os
import re
import sys, logging import sys, logging
from pathlib import Path from pathlib import Path
from pprint import pformat from pprint import pformat
@@ -81,6 +82,8 @@ class ProcedureCreation(QDialog):
equipmentrole['equipment'].index(item_in_er_list))) equipmentrole['equipment'].index(item_in_er_list)))
proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True) proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True)
self.update_equipment = EquipmentUsage.update_equipment 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( html = render_details_template(
template_name="procedure_creation", template_name="procedure_creation",
# css_in=['new_context_menu'], # css_in=['new_context_menu'],
@@ -91,8 +94,8 @@ class ProcedureCreation(QDialog):
plate_map=self.plate_map, plate_map=self.plate_map,
edit=self.edit edit=self.edit
) )
with open("procedure_creation_rendered.html", "w") as f: # with open("procedure_creation_rendered.html", "w") as f:
f.write(html) # f.write(html)
self.webview.setHtml(html) self.webview.setHtml(html)
@pyqtSlot(str, str, str, str) @pyqtSlot(str, str, str, str)
@@ -127,11 +130,18 @@ class ProcedureCreation(QDialog):
setattr(self.procedure.run, key, new_value) setattr(self.procedure.run, key, new_value)
case _: case _:
attribute = getattr(self.procedure, key) attribute = getattr(self.procedure, key)
attribute['value'] = new_value.strip('\"') 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) @pyqtSlot(str, bool)
def check_toggle(self, key: str, ischecked: 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) setattr(self.procedure, key, ischecked)
@pyqtSlot(str) @pyqtSlot(str)
@@ -159,7 +169,7 @@ class ProcedureCreation(QDialog):
self.set_html() self.set_html()
@pyqtSlot(str, str) @pyqtSlot(str, str)
def update_reagent(self, reagentrole:str, name_lot_expiry:str): def update_reagent(self, reagentrole: str, name_lot_expiry: str):
try: try:
name, lot, expiry = name_lot_expiry.split(" - ") name, lot, expiry = name_lot_expiry.split(" - ")
except ValueError as e: except ValueError as e:
@@ -167,8 +177,8 @@ class ProcedureCreation(QDialog):
return return
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
def return_sql(self): def return_sql(self, new: bool = False):
return self.procedure.to_sql() return self.procedure.to_sql(new=new)
# class ProcedureWebViewer(QWebEngineView): # class ProcedureWebViewer(QWebEngineView):
# #

View File

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

View File

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

View File

@@ -150,9 +150,15 @@ ul.no-bullets {
.nested { .nested {
margin-left: 50px; margin-left: 50px;
padding: 0 18px; padding: 0 18px;
display: none;
overflow: hidden; overflow: hidden;
background-color: #f1f1f1; background-color: #f1f1f1;
.nested {
background-color: #ffffff;
}
}
.hidden_input {
display: none;
} }
/* Style the button that is used to open and close the collapsible content */ /* 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 changed_it = new Event('change');
var reagentRoles = document.getElementsByClassName("reagentrole"); var reagentRoles = document.getElementsByClassName("reagentrole");

View File

@@ -23,7 +23,12 @@
<label for="technician">Technician:</label><br> <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> <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> <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> <label>Kit Type:</label><br>
<select class="dropdown" id="kittype" background-colour="{{ procedure['kittype']['colour'] }}"> <select class="dropdown" id="kittype" background-colour="{{ procedure['kittype']['colour'] }}">
{% for kittype in procedure['possible_kits'] %} {% for kittype in procedure['possible_kits'] %}

View File

@@ -1,6 +1,5 @@
{% extends "details.html" %} {% extends "details.html" %}
{% if not child %} {% if not child %}
<head> <head>
{% block head %} {% block head %}
{{ super() }} {{ super() }}
@@ -17,7 +16,7 @@
{% endfor %}</p> {% endfor %}</p>
{% if procedure['reagent'] %} {% if procedure['reagent'] %}
<button type="button"><h3><u>Reagents:</u></h3></button> <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> <tr>
<th style="border: 1px solid black;">Reagent Role</th> <th style="border: 1px solid black;">Reagent Role</th>
<th style="border: 1px solid black;">Reagent Name</th> <th style="border: 1px solid black;">Reagent Name</th>
@@ -28,12 +27,12 @@
{% with reagent=reg, child=True %} {% with reagent=reg, child=True %}
{% include "support/reagent_list.html" %} {% include "support/reagent_list.html" %}
{% endwith %} {% endwith %}
{% endfor %}</p> {% endfor %}
</table><br/> </table><br/>
{% endif %} {% endif %}
{% if procedure['equipment'] %} {% if procedure['equipment'] %}
<button type="button"><h3><u>Equipment:</u></h3></button> <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> <tr>
<th style="border: 1px solid black;">Equipment Role</th> <th style="border: 1px solid black;">Equipment Role</th>
<th style="border: 1px solid black;">Equipment Name</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> &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> {% endfor %}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% if not child %}
</body> </body>
{% endif %}

View File

@@ -1,4 +1,5 @@
{% extends "details.html" %} {% extends "details.html" %}
{% if not child %}
<head> <head>
{% block head %} {% block head %}
@@ -7,7 +8,7 @@
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% endif %}
{% block body %} {% block body %}
<h2><u>Run Details for {{ run['rsl_plate_number'] }}</u></h2> <h2><u>Run Details for {{ run['rsl_plate_number'] }}</u></h2>
{{ super() }} {{ super() }}
@@ -22,11 +23,13 @@
{% endif %} {% endif %}
{% if run['procedure'] %} {% if run['procedure'] %}
<button type="button"><h3><u>Procedures:</u></h3></button> <button type="button"><h3><u>Procedures:</u></h3></button>
<div class="nested">
{% for procedure in run['procedure'] %} {% for procedure in run['procedure'] %}
{% with procedure=procedure, child=True %} {% with procedure=procedure, child=True %}
{% include "procedure_details.html" %} {% include "procedure_details.html" %}
{% endwith %} {% endwith %}
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block signing_button %} {% block signing_button %}
@@ -34,7 +37,9 @@
{% endblock %} {% endblock %}
<br> <br>
{% if not child %}
</body> </body>
{% endif %}
{% block script %} {% block script %}
{{ super() }} {{ 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: if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment:
target_cell.comment = copy(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): def setup_lookup(func):
""" """
Checks to make sure all args are allowed Checks to make sure all args are allowed