diff --git a/TODO.md b/TODO.md index 674a6f9..5088558 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- [ ] Fix updating of Extraction Kit in submission form widget. - [x] Fix cropping of gel image. - [ ] Create Tips ... *sigh*. - [x] Create platemap image from html for export to pdf. diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index 59282e5..b0bb685 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -1,6 +1,7 @@ # __init__.py from pathlib import Path +from tools import CustomFormatter # Version of the realpython-reader package __project__ = "submissions" @@ -10,16 +11,7 @@ __copyright__ = "2022-2024, Government of Canada" project_path = Path(__file__).parents[2].absolute() -class bcolors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKCYAN = '\033[96m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' +bcolors = CustomFormatter.bcolors() # Hello Landon, this is your past self here. I'm trying not to screw you over like I usually do, so I will # set out the workflow I've imagined for creating new submission types. diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 136da9a..ee42ad0 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -372,7 +372,9 @@ class BasicSubmission(BaseClass): sample, _ = sample.toSQL(submission=self) return case "reagents": + logger.debug(f"Reagents coming into SQL: {value}") field_value = [reagent['value'].toSQL()[0] if isinstance(reagent, dict) else reagent.toSQL()[0] for reagent in value] + logger.debug(f"Reagents coming out of SQL: {field_value}") case "submission_type": field_value = SubmissionType.query(name=value) case "sample_count": @@ -392,8 +394,8 @@ class BasicSubmission(BaseClass): # insert into field try: self.__setattr__(key, field_value) - except AttributeError: - logger.error(f"Could not set {self} attribute {key} to {value}") + except AttributeError as e: + logger.error(f"Could not set {self} attribute {key} to {value} due to \n{e}") def update_subsampassoc(self, sample:BasicSample, input_dict:dict): """ diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 763fe50..f8b888c 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -441,6 +441,9 @@ class PydSubmission(BaseModel, extra='allow'): output.append(sample) return output + def set_attribute(self, key, value): + self.__setattr__(name=key, value=value) + def handle_duplicate_samples(self): """ Collapses multiple samples with same submitter id into one with lists for rows, columns. @@ -577,7 +580,7 @@ class PydSubmission(BaseModel, extra='allow'): SubmissionFormWidget: Submission form widget """ from frontend.widgets.submission_widget import SubmissionFormWidget - return SubmissionFormWidget(parent=parent, **self.improved_dict()) + return SubmissionFormWidget(parent=parent, submission=self) def autofill_excel(self, missing_only:bool=True, backup:bool=False) -> Workbook: """ @@ -782,7 +785,7 @@ class PydSubmission(BaseModel, extra='allow'): # logger.debug(f"Template rendered as: {render}") return render - def check_kit_integrity(self, reagenttypes:list=[]) -> Report: + def check_kit_integrity(self, reagenttypes:list=[], extraction_kit:str|dict|None=None) -> Tuple[List[PydReagent], Report]: """ Ensures all reagents expected in kit are listed in Submission @@ -793,24 +796,43 @@ class PydSubmission(BaseModel, extra='allow'): Report: Result object containing a message and any missing components. """ report = Report() + logger.debug(f"Extraction kit: {extraction_kit}. Is it a string? {isinstance(extraction_kit, str)}") + if isinstance(extraction_kit, str): + extraction_kit = dict(value=extraction_kit) + if extraction_kit is not None: + if extraction_kit != self.extraction_kit['value']: + self.extraction_kit['value'] = extraction_kit['value'] + reagenttypes = [] + else: + reagenttypes = [item.type for item in self.reagents] + else: + reagenttypes = [item.type for item in self.reagents] + logger.debug(f"Looking up {self.extraction_kit['value']}") ext_kit = KitType.query(name=self.extraction_kit['value']) - ext_kit_rtypes = [item.name for item in ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])] - reagenttypes = [item.type for item in self.reagents] + ext_kit_rtypes = [item.to_pydantic() for item in ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])] logger.debug(f"Kit reagents: {ext_kit_rtypes}") - logger.debug(f"Submission reagents: {reagenttypes}") + logger.debug(f"Submission reagents: {self.reagents}") # check if lists are equal - check = set(ext_kit_rtypes) == set(reagenttypes) - logger.debug(f"Checking if reagents match kit contents: {check}") - # what reagent types are in both lists? - missing = list(set(ext_kit_rtypes).difference(reagenttypes)) + # check = set(ext_kit_rtypes) == set(reagenttypes) + # logger.debug(f"Checking if reagents match kit contents: {check}") + # # what reagent types are in both lists? + # missing = list(set(ext_kit_rtypes).difference(reagenttypes)) + missing = [] + output_reagents = self.reagents + logger.debug(f"Already have these reagent types: {reagenttypes}") + for rt in ext_kit_rtypes: + if rt.type not in reagenttypes: + missing.append(rt) + if rt.type not in [item.type for item in output_reagents]: + output_reagents.append(rt) logger.debug(f"Missing reagents types: {missing}") # if lists are equal return no problem if len(missing)==0: result = None else: - result = Result(msg=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning") + result = Result(msg=f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", status="Warning") report.add_result(result) - return report + return output_reagents, report class PydContact(BaseModel): diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 03a2e88..c74d510 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -205,7 +205,7 @@ class SubmissionFormContainer(QWidget): info = dlg.parse_form() logger.debug(f"Reagent info: {info}") # create reagent object - reagent = PydReagent(ctx=self.ctx, **info) + reagent = PydReagent(ctx=self.app.ctx, **info) # send reagent to db sqlobj, result = reagent.toSQL() sqlobj.save() @@ -215,31 +215,39 @@ class SubmissionFormContainer(QWidget): class SubmissionFormWidget(QWidget): - def __init__(self, parent: QWidget, **kwargs) -> None: + def __init__(self, parent: QWidget, submission:PydSubmission) -> None: super().__init__(parent) self.report = Report() self.app = parent.app + self.pyd = submission # self.input = [{k:v} for k,v in kwargs.items()] - self.samples = [] + # self.samples = [] self.missing_info = [] self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment', 'equipment', 'source_plates', 'id', 'cost', 'extraction_info', 'controls', 'pcr_info', 'gel_info', 'gel_image'] self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment'] self.layout = QVBoxLayout() - for k, v in kwargs.items(): + # for k, v in kwargs.items(): + for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()): if k not in self.ignore: - add_widget = self.create_widget(key=k, value=v, submission_type=kwargs['submission_type']) + try: + value = self.pyd.__getattribute__(k) + except AttributeError: + logger.error(f"Couldn't get attribute from pyd: {k}") + value = dict(value=None, missing=True) + add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value']) if add_widget != None: self.layout.addWidget(add_widget) if k == "extraction_kit": add_widget.input.currentTextChanged.connect(self.scrape_reagents) # else: - self.__setattr__(k, v) - self.scrape_reagents(self.extraction_kit['value']) + # self.__setattr__(k, v) + # self.scrape_reagents(self.extraction_kit['value']) + self.scrape_reagents(self.pyd.extraction_kit) # extraction kit must be added last so widget order makes sense. # self.layout.addWidget(self.create_widget(key="extraction_kit", value=self.extraction_kit, submission_type=self.submission_type)) - if hasattr(self, "csv"): + if hasattr(self.pyd, "csv"): export_csv_btn = QPushButton("Export CSV") export_csv_btn.setObjectName("export_csv_btn") self.layout.addWidget(export_csv_btn) @@ -252,7 +260,7 @@ class SubmissionFormWidget(QWidget): self.app.report.add_result(self.report) self.app.result_reporter() - def create_widget(self, key:str, value:dict, submission_type:str|None=None) -> "self.InfoItem": + def create_widget(self, key:str, value:dict|PydReagent, submission_type:str|None=None, extraction_kit:str|None=None) -> "self.InfoItem": """ Make an InfoItem widget to hold a field @@ -265,7 +273,11 @@ class SubmissionFormWidget(QWidget): self.InfoItem: Form widget to hold name:value """ if key not in self.ignore: - widget = self.InfoItem(self, key=key, value=value, submission_type=submission_type) + match value: + case PydReagent(): + widget = self.ReagentFormWidget(self, reagent=value, extraction_kit=extraction_kit) + case _: + widget = self.InfoItem(self, key=key, value=value, submission_type=submission_type) return widget return None @@ -304,15 +316,19 @@ class SubmissionFormWidget(QWidget): # self.reagents = self.prsr.sub['reagents'] # case _: # already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing] - already_have = [reagent for reagent in self.reagents if not reagent.missing] - names = list(set([item.type for item in already_have])) - # logger.debug(f"Already have: {already_have}") - reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.submission_type['value']) if item.name not in names] - # logger.debug(f"Missing: {reagents}") - self.reagents = already_have + reagents + # already_have = [reagent for reagent in self.pyd.reagents if not reagent.missing] + # names = list(set([item.type for item in already_have])) + # # logger.debug(f"Already have: {already_have}") + # reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.pyd.submission_type) if item.name not in names] + # # logger.debug(f"Missing: {reagents}") + # self.pyd.reagents = already_have + reagents # logger.debug(f"Reagents: {self.reagents}") - self.kit_integrity_completion_function(extraction_kit=extraction_kit) + # self.kit_integrity_completion_function(extraction_kit=extraction_kit) + reagents, report = self.pyd.check_kit_integrity(extraction_kit=extraction_kit) # logger.debug(f"Missing reagents: {obj.missing_reagents}") + for reagent in reagents: + add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit) + self.layout.addWidget(add_widget) self.report.add_result(report) logger.debug(f"Outgoing report: {self.report.results}") @@ -391,15 +407,18 @@ class SubmissionFormWidget(QWidget): """ logger.debug(f"\n\nBeginning Submission\n\n") report = Report() - self.pyd: PydSubmission = self.parse_form() + # self.pyd: PydSubmission = self.parse_form() + result = self.parse_form() + report.add_result(result) logger.debug(f"Submission: {pformat(self.pyd)}") logger.debug("Checking kit integrity...") - result = self.pyd.check_kit_integrity() + _, result = self.pyd.check_kit_integrity() report.add_result(result) if len(result.results) > 0: self.app.report.add_result(report) self.app.result_reporter() return + logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n") base_submission, result = self.pyd.toSQL() # logger.debug(f"Base submission: {base_submission.to_dict()}") # check output message for issues @@ -465,6 +484,7 @@ class SubmissionFormWidget(QWidget): Returns: PydSubmission: Pydantic submission object """ + report = Report() logger.debug(f"Hello from form parser!") info = {} reagents = [] @@ -481,6 +501,7 @@ class SubmissionFormWidget(QWidget): info[field] = value logger.debug(f"Info: {pformat(info)}") logger.debug(f"Reagents: {pformat(reagents)}") + self.pyd.reagents = reagents # logger.debug(f"Attrs not in info: {[k for k, v in self.__dict__.items() if k not in info.keys()]}") for item in self.recover: logger.debug(f"Attempting to recover: {item}") @@ -488,8 +509,11 @@ class SubmissionFormWidget(QWidget): value = getattr(self, item) logger.debug(f"Setting {item}") info[item] = value - submission = PydSubmission(reagents=reagents, **info) - return submission + # submission = PydSubmission(reagents=reagents, **info) + for k,v in info.items(): + self.pyd.set_attribute(key=k, value=v) + # return submission + self.report.add_result(report) class InfoItem(QWidget): @@ -497,7 +521,7 @@ class SubmissionFormWidget(QWidget): super().__init__(parent) layout = QVBoxLayout() self.label = self.ParsedQLabel(key=key, value=value) - self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type['value']) + self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type) self.setObjectName(key) try: self.missing:bool = value['missing'] @@ -684,8 +708,9 @@ class SubmissionFormWidget(QWidget): Tuple[PydReagent, dict]: PydReagent and Report(?) """ lot = self.lot.currentText() + logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}") wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type) - # if reagent doesn't exist in database, off to add it (uses App.add_reagent) + # if reagent doesn't exist in database, offer to add it (uses App.add_reagent) if wanted_reagent == None: dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?") if dlg.exec(): @@ -764,19 +789,20 @@ class SubmissionFormWidget(QWidget): looked_up_reg = None if isinstance(looked_up_reg, list): looked_up_reg = None - logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}") + # logger.debug(f"Because there was no reagent listed for {reagent.lot}, we will insert the last lot used: {looked_up_reg}") if looked_up_reg != None: relevant_reagents.remove(str(looked_up_reg.lot)) relevant_reagents.insert(0, str(looked_up_reg.lot)) else: if len(relevant_reagents) > 1: - logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.") + # logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. Moving to front of list.") idx = relevant_reagents.index(str(reagent.lot)) - logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}") + # logger.debug(f"The index we got for {reagent.lot} in {relevant_reagents} was {idx}") moved_reag = relevant_reagents.pop(idx) relevant_reagents.insert(0, moved_reag) else: - logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.") - logger.debug(f"New relevant reagents: {relevant_reagents}") + # logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.") + pass + # logger.debug(f"New relevant reagents: {relevant_reagents}") self.setObjectName(f"lot_{reagent.type}") self.addItems(relevant_reagents) diff --git a/src/submissions/tools.py b/src/submissions/tools.py index 14f7ed2..8c539f9 100644 --- a/src/submissions/tools.py +++ b/src/submissions/tools.py @@ -14,7 +14,6 @@ from sqlalchemy import create_engine from pydantic import field_validator, BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Any, Tuple, Literal, List -from __init__ import bcolors logger = logging.getLogger(f"submissions.{__name__}") @@ -296,6 +295,17 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler): class CustomFormatter(logging.Formatter): + class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s" FORMATS = {