diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 253d86f..073165c 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -283,6 +283,17 @@ class BasicSubmission(BaseClass): del input_dict['id'] return input_dict + def generate_associations(self, name:str, extra:str|None=None): + try: + field = self.__getattribute__(name) + except AttributeError: + return None + for item in field: + if extra: + yield item.to_sub_dict(extra) + else: + yield item.to_sub_dict() + def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict: """ Constructs dictionary used in submissions summary @@ -332,6 +343,10 @@ class BasicSubmission(BaseClass): try: reagents = [item.to_sub_dict(extraction_kit=self.extraction_kit) for item in self.submission_reagent_associations] + except Exception as e: + logger.error(f"We got an error retrieving reagents: {e}") + reagents = [] + finally: for k, v in self.extraction_kit.construct_xl_map_for_use(self.submission_type): if k == 'info': continue @@ -341,26 +356,26 @@ class BasicSubmission(BaseClass): reagents.append( dict(role=k, name="Not Applicable", lot="NA", expiry=expiry, missing=True)) - except Exception as e: - logger.error(f"We got an error retrieving reagents: {e}") - reagents = None # logger.debug(f"Running samples.") - samples = self.adjust_to_dict_samples(backup=backup) + # samples = self.adjust_to_dict_samples(backup=backup) + samples = self.generate_associations(name="submission_sample_associations") # logger.debug("Running equipment") - try: - equipment = [item.to_sub_dict() for item in self.submission_equipment_associations] - if not equipment: - equipment = None - except Exception as e: - logger.error(f"Error setting equipment: {e}") - equipment = None - try: - tips = [item.to_sub_dict() for item in self.submission_tips_associations] - if not tips: - tips = None - except Exception as e: - logger.error(f"Error setting tips: {e}") - tips = None + equipment = self.generate_associations(name="submission_equipment_associations") + # try: + # equipment = [item.to_sub_dict() for item in self.submission_equipment_associations] + # if not equipment: + # equipment = None + # except Exception as e: + # logger.error(f"Error setting equipment: {e}") + # equipment = None + tips = self.generate_associations(name="submission_tips_associations") + # try: + # tips = [item.to_sub_dict() for item in self.submission_tips_associations] + # if not tips: + # tips = None + # except Exception as e: + # logger.error(f"Error setting tips: {e}") + # tips = None cost_centre = self.cost_centre custom = self.custom else: @@ -1013,18 +1028,18 @@ class BasicSubmission(BaseClass): logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler") return samples - def adjust_to_dict_samples(self, backup: bool = False) -> List[dict]: - """ - Updates sample dictionaries with custom values - - Args: - backup (bool, optional): Whether to perform backup. Defaults to False. - - Returns: - List[dict]: Updated dictionaries - """ - # logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.") - return [item.to_sub_dict() for item in self.submission_sample_associations] + # def adjust_to_dict_samples(self, backup: bool = False) -> List[dict]: + # """ + # Updates sample dictionaries with custom values + # + # Args: + # backup (bool, optional): Whether to perform backup. Defaults to False. + # + # Returns: + # List[dict]: Updated dictionaries + # """ + # # logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.") + # return [item.to_sub_dict() for item in self.submission_sample_associations] @classmethod def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]: diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index fd1b662..5a1d87a 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -53,7 +53,6 @@ class SheetParser(object): self.parse_samples() self.parse_equipment() self.parse_tips() - # self.finalize_parse() # logger.debug(f"Parser.sub after info scrape: {pformat(self.sub)}") def parse_info(self): @@ -63,6 +62,8 @@ class SheetParser(object): parser = InfoParser(xl=self.xl, submission_type=self.submission_type, sub_object=self.sub_object) info = parser.parse_info() self.info_map = parser.map + # NOTE: in order to accommodate generic submission types we have to check for the type in the excel sheet and + # rerun accordingly try: check = info['submission_type']['value'] not in [None, "None", "", " "] except KeyError: @@ -78,13 +79,15 @@ class SheetParser(object): else: self.submission_type = RSLNamer.retrieve_submission_type(filename=self.filepath) self.parse_info() - for k, v in info.items(): - match k: - # NOTE: exclude samples. - case "sample": - continue - case _: - self.sub[k] = v + [self.sub.__setitem__(k, v) for k, v in info.items()] + # for k, v in info.items(): + # match k: + # # NOTE: exclude samples. + # case "sample": + # logger.debug(f"Sample found: {k}: {v}") + # continue + # case _: + # self.sub[k] = v def parse_reagents(self, extraction_kit: str | None = None): """ @@ -105,7 +108,7 @@ class SheetParser(object): Calls sample parser to pull info from the excel sheet """ parser = SampleParser(xl=self.xl, submission_type=self.submission_type) - self.sub['samples'] = parser.reconcile_samples() + self.sub['samples'] = parser.parse_samples() def parse_equipment(self): """ @@ -145,29 +148,29 @@ class SheetParser(object): PydSubmission: output pydantic model """ # logger.debug(f"Submission dictionary coming into 'to_pydantic':\n{pformat(self.sub)}") - pyd_dict = copy(self.sub) - pyd_dict['samples'] = [PydSample(**sample) for sample in self.sub['samples']] + # pyd_dict = copy(self.sub) + # self.sub['samples'] = [PydSample(**sample) for sample in self.sub['samples']] # logger.debug(f"Reagents: {pformat(self.sub['reagents'])}") - pyd_dict['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']] + # self.sub['reagents'] = [PydReagent(**reagent) for reagent in self.sub['reagents']] # logger.debug(f"Equipment: {self.sub['equipment']}") - try: - check = bool(self.sub['equipment']) - except TypeError: - check = False - if check: - pyd_dict['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']] - else: - pyd_dict['equipment'] = None - try: - check = bool(self.sub['tips']) - except TypeError: - check = False - if check: - pyd_dict['tips'] = [PydTips(**tips) for tips in self.sub['tips']] - else: - pyd_dict['tips'] = None - psm = PydSubmission(filepath=self.filepath, run_custom=True, **pyd_dict) - return psm + # try: + # check = bool(self.sub['equipment']) + # except TypeError: + # check = False + # if check: + # self.sub['equipment'] = [PydEquipment(**equipment) for equipment in self.sub['equipment']] + # else: + # self.sub['equipment'] = None + # try: + # check = bool(self.sub['tips']) + # except TypeError: + # check = False + # if check: + # self.sub['tips'] = [PydTips(**tips) for tips in self.sub['tips']] + # else: + # self.sub['tips'] = None + return PydSubmission(filepath=self.filepath, run_custom=True, **self.sub) + # return psm class InfoParser(object): @@ -287,7 +290,7 @@ class ReagentParser(object): extraction_kit (str): Extraction kit used. sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None. """ - # logger.debug("\n\nHello from ReagentParser!\n\n") + logger.debug("\n\nHello from ReagentParser!\n\n") if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) self.submission_type_obj = submission_type @@ -382,7 +385,7 @@ class SampleParser(object): sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None. sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None. """ - # logger.debug("\n\nHello from SampleParser!\n\n") + logger.debug("\n\nHello from SampleParser!\n\n") self.samples = [] self.xl = xl if isinstance(submission_type, str): @@ -477,49 +480,50 @@ class SampleParser(object): lookup_samples.append(self.samp_object.parse_sample(row_dict)) return lookup_samples - def parse_samples(self) -> Tuple[Report | None, List[dict] | List[PydSample]]: - """ - Parse merged platemap/lookup info into dicts/samples + # def parse_samples(self) -> Tuple[Report | None, List[dict] | List[PydSample]]: + # """ + # Parse merged platemap/lookup info into dicts/samples + # + # Returns: + # List[dict]|List[models.BasicSample]: List of samples + # """ + # result = None + # new_samples = [] + # # logger.debug(f"Starting samples: {pformat(self.samples)}") + # for sample in self.samples: + # translated_dict = {} + # for k, v in sample.items(): + # match v: + # case dict(): + # v = None + # case float(): + # v = convert_nans_to_nones(v) + # case _: + # v = v + # translated_dict[k] = convert_nans_to_nones(v) + # translated_dict['sample_type'] = f"{self.submission_type} Sample" + # translated_dict = self.sub_object.parse_samples(translated_dict) + # translated_dict = self.samp_object.parse_sample(translated_dict) + # # logger.debug(f"Here is the output of the custom parser:\n{translated_dict}") + # new_samples.append(PydSample(**translated_dict)) + # return result, new_samples - Returns: - List[dict]|List[models.BasicSample]: List of samples - """ - result = None - new_samples = [] - # logger.debug(f"Starting samples: {pformat(self.samples)}") - for sample in self.samples: - translated_dict = {} - for k, v in sample.items(): - match v: - case dict(): - v = None - case float(): - v = convert_nans_to_nones(v) - case _: - v = v - translated_dict[k] = convert_nans_to_nones(v) - translated_dict['sample_type'] = f"{self.submission_type} Sample" - translated_dict = self.sub_object.parse_samples(translated_dict) - translated_dict = self.samp_object.parse_sample(translated_dict) - # logger.debug(f"Here is the output of the custom parser:\n{translated_dict}") - new_samples.append(PydSample(**translated_dict)) - return result, new_samples - - def reconcile_samples(self) -> Generator[dict, None, None]: + def parse_samples(self) -> Generator[dict, None, None]: """ Merges sample info from lookup table and plate map. Returns: List[dict]: Reconciled samples """ - if self.plate_map_samples is None or self.lookup_samples is None: + if not self.plate_map_samples or not self.lookup_samples: + logger.error(f"No separate samples, returning") self.samples = self.lookup_samples or self.plate_map_samples return merge_on_id = self.sample_info_map['lookup_table']['merge_on_id'] plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id']) lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id]) - print(pformat(plate_map_samples)) - print(pformat(lookup_samples)) + # print(pformat(plate_map_samples)) + # print(pformat(lookup_samples)) for ii, psample in enumerate(plate_map_samples): try: check = psample['id'] == lookup_samples[ii][merge_on_id] @@ -563,6 +567,7 @@ class EquipmentParser(object): xl (Workbook): Openpyxl workbook from submitted excel file. submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) """ + logger.debug("\n\nHello from EquipmentParser!\n\n") if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) self.submission_type = submission_type @@ -649,6 +654,7 @@ class TipParser(object): xl (Workbook): Openpyxl workbook from submitted excel file. submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) """ + logger.debug("\n\nHello from TipParser!\n\n") if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) self.submission_type = submission_type diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 5ddb973..82e29ac 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta from dateutil.parser import parse from dateutil.parser import ParserError from typing import List, Tuple, Literal +from types import GeneratorType from . import RSLNamer from pathlib import Path from tools import check_not_nan, convert_nans_to_nones, Report, Result @@ -395,16 +396,32 @@ class PydSubmission(BaseModel, extra='allow'): submission_category: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) comment: dict | None = Field(default=dict(value="", missing=True), validate_default=True) reagents: List[dict] | List[PydReagent] = [] - samples: List[PydSample] + samples: List[PydSample] | Generator equipment: List[PydEquipment] | None = [] cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) contact: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) + tips: List[PydTips] | None =[] + + @field_validator("tips", mode="before") + @classmethod + def expand_tips(cls, value): + # print(f"\n{type(value)}\n") + if isinstance(value, dict): + value = value['value'] + if isinstance(value, Generator): + logger.debug("We have a 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): # logger.debug(f"Equipment: {value}") - + if isinstance(value, Generator): + logger.debug("We have a generator") + return [PydEquipment(**equipment) for equipment in value] if isinstance(value, dict): return value['value'] return value @@ -580,6 +597,24 @@ class PydSubmission(BaseModel, extra='allow'): value['value'] = values.data['submission_type']['value'] return value + @field_validator("reagents", mode="before") + @classmethod + def expand_reagents(cls, value): + # print(f"\n{type(value)}\n") + if isinstance(value, Generator): + logger.debug("We have a generator") + return [PydReagent(**reagent) for reagent in value] + return value + + @field_validator("samples", mode="before") + @classmethod + def expand_samples(cls, value): + # print(f"\n{type(value)}\n") + if isinstance(value, Generator): + logger.debug("We have a generator") + return [PydSample(**sample) for sample in value] + return value + @field_validator("samples") @classmethod def assign_ids(cls, value): diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 46e29db..30e2a51 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -93,15 +93,16 @@ class SubmissionDetails(QDialog): Args: sample (str): Submitter Id of the sample. """ + logger.debug(f"Details: {sample}") if isinstance(sample, str): sample = BasicSample.query(submitter_id=sample) base_dict = sample.to_sub_dict(full_data=True) exclude = ['submissions', 'excluded', 'colour', 'tooltip'] - try: - base_dict['excluded'] += exclude - except KeyError: - base_dict['excluded'] = exclude - template = sample.get_details_template(base_dict=base_dict) + # try: + # base_dict['excluded'] += exclude + # except KeyError: + base_dict['excluded'] = exclude + template = sample.get_details_template() template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0]) with open(template_path.joinpath("css", "styles.css"), "r") as f: css = f.read() @@ -158,6 +159,8 @@ class SubmissionDetails(QDialog): # logger.debug(f"Submission_details: {pformat(self.base_dict)}") # logger.debug(f"User is power user: {is_power_user()}") self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css) + with open("test.html", "w") as f: + f.write(self.html) self.webview.setHtml(self.html) diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicsubmission_details.html index 19c979c..fa00580 100644 --- a/src/submissions/templates/basicsubmission_details.html +++ b/src/submissions/templates/basicsubmission_details.html @@ -20,7 +20,7 @@

Reagents:

{% for item in sub['reagents'] %} -     {{ item['role'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
+     {{ item['role'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
{% endfor %}

{% if sub['equipment'] %} @@ -38,7 +38,7 @@ {% if sub['samples'] %}

Samples:

{% for item in sub['samples'] %} -     {{ item['well'] }}:{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '
        ') }}){% else %} {{ item['name']|replace('\n\t', '
        ') }}{% endif %}

+     {{ item['well'] }}:{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '
        ') }}){% else %} {{ item['name']|replace('\n\t', '
        ') }}{% endif %}

{% endfor %}

{% endif %} @@ -83,22 +83,25 @@ diff --git a/src/submissions/templates/plate_map.html b/src/submissions/templates/plate_map.html index 87e38c1..dbc867c 100644 --- a/src/submissions/templates/plate_map.html +++ b/src/submissions/templates/plate_map.html @@ -1,6 +1,6 @@