From b9ca9586ec504f71adb0bf3286d76eb2afcfc4c6 Mon Sep 17 00:00:00 2001 From: lwark Date: Tue, 22 Jul 2025 13:15:24 -0500 Subject: [PATCH] Writer and manager updates. --- src/submissions/backend/db/models/kits.py | 38 ++- .../backend/db/models/submissions.py | 24 +- .../results_parsers/pcr_results_parser.py | 68 ++--- .../backend/excel/writers/__init__.py | 175 ++++++++++++ .../excel/writers/clientsubmission_writer.py | 73 +++++ .../writers/procedure_writers/__init__.py | 97 +++++++ .../backend/managers/clientsubmissions.py | 62 +++++ .../backend/managers/results/__init__.py | 16 ++ .../managers/results/pcr_results_manager.py | 25 +- src/submissions/backend/managers/runs.py | 28 ++ src/submissions/backend/validators/pydant.py | 258 +++--------------- .../frontend/widgets/procedure_creation.py | 24 +- .../frontend/widgets/submission_details.py | 18 +- .../templates/clientsubmission_details.html | 9 +- src/submissions/templates/css/styles.css | 8 +- .../templates/js/procedure_form.js | 18 ++ .../templates/procedure_creation.html | 7 +- .../templates/procedure_details.html | 10 +- src/submissions/templates/run_details.html | 7 +- .../templates/support/equipment_list.html | 10 + src/submissions/tools/__init__.py | 15 + 21 files changed, 661 insertions(+), 329 deletions(-) create mode 100644 src/submissions/backend/excel/writers/__init__.py create mode 100644 src/submissions/backend/excel/writers/clientsubmission_writer.py create mode 100644 src/submissions/backend/excel/writers/procedure_writers/__init__.py create mode 100644 src/submissions/backend/managers/clientsubmissions.py create mode 100644 src/submissions/backend/managers/runs.py create mode 100644 src/submissions/templates/support/equipment_list.html diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index ad5b4c0..1b40273 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -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") - 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] 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"" 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 "" return f"" 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: diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 2f1d7fb..52c275e 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -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() diff --git a/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py b/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py index 01b0720..2c59210 100644 --- a/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py +++ b/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py @@ -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 - diff --git a/src/submissions/backend/excel/writers/__init__.py b/src/submissions/backend/excel/writers/__init__.py new file mode 100644 index 0000000..a745a9e --- /dev/null +++ b/src/submissions/backend/excel/writers/__init__.py @@ -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 + + + + diff --git a/src/submissions/backend/excel/writers/clientsubmission_writer.py b/src/submissions/backend/excel/writers/clientsubmission_writer.py new file mode 100644 index 0000000..89846d8 --- /dev/null +++ b/src/submissions/backend/excel/writers/clientsubmission_writer.py @@ -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 + + + + diff --git a/src/submissions/backend/excel/writers/procedure_writers/__init__.py b/src/submissions/backend/excel/writers/procedure_writers/__init__.py new file mode 100644 index 0000000..a224bfa --- /dev/null +++ b/src/submissions/backend/excel/writers/procedure_writers/__init__.py @@ -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 diff --git a/src/submissions/backend/managers/clientsubmissions.py b/src/submissions/backend/managers/clientsubmissions.py new file mode 100644 index 0000000..fecd81d --- /dev/null +++ b/src/submissions/backend/managers/clientsubmissions.py @@ -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 \ No newline at end of file diff --git a/src/submissions/backend/managers/results/__init__.py b/src/submissions/backend/managers/results/__init__.py index d5bf697..2ed2d55 100644 --- a/src/submissions/backend/managers/results/__init__.py +++ b/src/submissions/backend/managers/results/__init__.py @@ -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 diff --git a/src/submissions/backend/managers/results/pcr_results_manager.py b/src/submissions/backend/managers/results/pcr_results_manager.py index 1876a6b..07ed53e 100644 --- a/src/submissions/backend/managers/results/pcr_results_manager.py +++ b/src/submissions/backend/managers/results/pcr_results_manager.py @@ -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() + + + diff --git a/src/submissions/backend/managers/runs.py b/src/submissions/backend/managers/runs.py new file mode 100644 index 0000000..96ca701 --- /dev/null +++ b/src/submissions/backend/managers/runs.py @@ -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 diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index ed3c518..8284710 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -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() - sql = super().to_sql() - # logger.debug(f"Initial PYD: {pformat(self.__dict__)}") + if new: + sql = Procedure() + else: + sql = super().to_sql() + 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. - removable = ProcedureReagentAssociation.query(procedure=sql, reagentrole=reagentrole) + 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: diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index badd694..fdb6d34 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -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) - 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) 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): # diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 6849235..f40b4b3 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -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: diff --git a/src/submissions/templates/clientsubmission_details.html b/src/submissions/templates/clientsubmission_details.html index 647a1ab..4dae1f2 100644 --- a/src/submissions/templates/clientsubmission_details.html +++ b/src/submissions/templates/clientsubmission_details.html @@ -11,27 +11,24 @@

Submission Details for {{ clientsubmission['name'] }}

{{ super() }}

{% for key, value in clientsubmission.items() if key not in clientsubmission['excluded'] %} -     {{ key | replace("_", " ") | title | replace("Id", "ID") }}: {% if key=='cost' %}{% if clientsubmission['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}
+     {{ key | replace("_", " ") | title | replace("Id", "ID") }}: {% if key=='cost' %}{% if clientsubmission['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}
{% endfor %}

- {% if clientsubmission['sample'] %} -

{% for sample in clientsubmission['sample'] %}     {{ sample['sample_id'] }}
{% endfor %}

- {% endif %} {% if clientsubmission['run'] %} - +
{% for run in clientsubmission['run'] %} {% with run=run, child=True %} {% include "run_details.html" %} {% endwith %} {% endfor %} - +
{% endif %} {% endblock %} diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css index 731865c..bfcb3b1 100644 --- a/src/submissions/templates/css/styles.css +++ b/src/submissions/templates/css/styles.css @@ -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 */ diff --git a/src/submissions/templates/js/procedure_form.js b/src/submissions/templates/js/procedure_form.js index 55dc1a6..05f631b 100644 --- a/src/submissions/templates/js/procedure_form.js +++ b/src/submissions/templates/js/procedure_form.js @@ -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"); diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index 35c8b05..105f10d 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -23,7 +23,12 @@


-

+
+