Writer and manager updates.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
175
src/submissions/backend/excel/writers/__init__.py
Normal file
175
src/submissions/backend/excel/writers/__init__.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
62
src/submissions/backend/managers/clientsubmissions.py
Normal file
62
src/submissions/backend/managers/clientsubmissions.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
28
src/submissions/backend/managers/runs.py
Normal file
28
src/submissions/backend/managers/runs.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
#
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'] %}
|
||||
<b>{{ key | replace("_", " ") | title | replace("Id", "ID") }}: </b>{% if key=='cost' %}{% if clientsubmission['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
||||
<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'] %}
|
||||
<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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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'] %}
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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 @@
|
||||
<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 %}
|
||||
|
||||
@@ -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() }}
|
||||
|
||||
10
src/submissions/templates/support/equipment_list.html
Normal file
10
src/submissions/templates/support/equipment_list.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user