Upgrade of editting functions.

This commit is contained in:
Landon Wark
2024-03-12 15:16:47 -05:00
parent badf6e21c8
commit 7cc9dd64c3
6 changed files with 105 additions and 52 deletions

View File

@@ -1,3 +1,4 @@
- [ ] Fix updating of Extraction Kit in submission form widget.
- [x] Fix cropping of gel image. - [x] Fix cropping of gel image.
- [ ] Create Tips ... *sigh*. - [ ] Create Tips ... *sigh*.
- [x] Create platemap image from html for export to pdf. - [x] Create platemap image from html for export to pdf.

View File

@@ -1,6 +1,7 @@
# __init__.py # __init__.py
from pathlib import Path from pathlib import Path
from tools import CustomFormatter
# Version of the realpython-reader package # Version of the realpython-reader package
__project__ = "submissions" __project__ = "submissions"
@@ -10,16 +11,7 @@ __copyright__ = "2022-2024, Government of Canada"
project_path = Path(__file__).parents[2].absolute() project_path = Path(__file__).parents[2].absolute()
class bcolors: bcolors = CustomFormatter.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'
# Hello Landon, this is your past self here. I'm trying not to screw you over like I usually do, so I will # 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. # set out the workflow I've imagined for creating new submission types.

View File

@@ -372,7 +372,9 @@ class BasicSubmission(BaseClass):
sample, _ = sample.toSQL(submission=self) sample, _ = sample.toSQL(submission=self)
return return
case "reagents": 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] 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": case "submission_type":
field_value = SubmissionType.query(name=value) field_value = SubmissionType.query(name=value)
case "sample_count": case "sample_count":
@@ -392,8 +394,8 @@ class BasicSubmission(BaseClass):
# insert into field # insert into field
try: try:
self.__setattr__(key, field_value) self.__setattr__(key, field_value)
except AttributeError: except AttributeError as e:
logger.error(f"Could not set {self} attribute {key} to {value}") logger.error(f"Could not set {self} attribute {key} to {value} due to \n{e}")
def update_subsampassoc(self, sample:BasicSample, input_dict:dict): def update_subsampassoc(self, sample:BasicSample, input_dict:dict):
""" """

View File

@@ -441,6 +441,9 @@ class PydSubmission(BaseModel, extra='allow'):
output.append(sample) output.append(sample)
return output return output
def set_attribute(self, key, value):
self.__setattr__(name=key, value=value)
def handle_duplicate_samples(self): def handle_duplicate_samples(self):
""" """
Collapses multiple samples with same submitter id into one with lists for rows, columns. 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 SubmissionFormWidget: Submission form widget
""" """
from frontend.widgets.submission_widget import SubmissionFormWidget 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: 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}") # logger.debug(f"Template rendered as: {render}")
return 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 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: Result object containing a message and any missing components.
""" """
report = Report() 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 = 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'])] ext_kit_rtypes = [item.to_pydantic() for item in ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])]
reagenttypes = [item.type for item in self.reagents]
logger.debug(f"Kit reagents: {ext_kit_rtypes}") 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 if lists are equal
check = set(ext_kit_rtypes) == set(reagenttypes) # check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}") # logger.debug(f"Checking if reagents match kit contents: {check}")
# what reagent types are in both lists? # # what reagent types are in both lists?
missing = list(set(ext_kit_rtypes).difference(reagenttypes)) # 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}") logger.debug(f"Missing reagents types: {missing}")
# if lists are equal return no problem # if lists are equal return no problem
if len(missing)==0: if len(missing)==0:
result = None result = None
else: 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) report.add_result(result)
return report return output_reagents, report
class PydContact(BaseModel): class PydContact(BaseModel):

View File

@@ -205,7 +205,7 @@ class SubmissionFormContainer(QWidget):
info = dlg.parse_form() info = dlg.parse_form()
logger.debug(f"Reagent info: {info}") logger.debug(f"Reagent info: {info}")
# create reagent object # create reagent object
reagent = PydReagent(ctx=self.ctx, **info) reagent = PydReagent(ctx=self.app.ctx, **info)
# send reagent to db # send reagent to db
sqlobj, result = reagent.toSQL() sqlobj, result = reagent.toSQL()
sqlobj.save() sqlobj.save()
@@ -215,31 +215,39 @@ class SubmissionFormContainer(QWidget):
class SubmissionFormWidget(QWidget): class SubmissionFormWidget(QWidget):
def __init__(self, parent: QWidget, **kwargs) -> None: def __init__(self, parent: QWidget, submission:PydSubmission) -> None:
super().__init__(parent) super().__init__(parent)
self.report = Report() self.report = Report()
self.app = parent.app self.app = parent.app
self.pyd = submission
# self.input = [{k:v} for k,v in kwargs.items()] # self.input = [{k:v} for k,v in kwargs.items()]
self.samples = [] # self.samples = []
self.missing_info = [] self.missing_info = []
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment', self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment',
'equipment', 'source_plates', 'id', 'cost', 'extraction_info', 'equipment', 'source_plates', 'id', 'cost', 'extraction_info',
'controls', 'pcr_info', 'gel_info', 'gel_image'] 'controls', 'pcr_info', 'gel_info', 'gel_image']
self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment'] self.recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
self.layout = QVBoxLayout() 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: 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: if add_widget != None:
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
if k == "extraction_kit": if k == "extraction_kit":
add_widget.input.currentTextChanged.connect(self.scrape_reagents) add_widget.input.currentTextChanged.connect(self.scrape_reagents)
# else: # else:
self.__setattr__(k, v) # self.__setattr__(k, v)
self.scrape_reagents(self.extraction_kit['value']) # 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. # 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)) # 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 = QPushButton("Export CSV")
export_csv_btn.setObjectName("export_csv_btn") export_csv_btn.setObjectName("export_csv_btn")
self.layout.addWidget(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.report.add_result(self.report)
self.app.result_reporter() 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 Make an InfoItem widget to hold a field
@@ -265,7 +273,11 @@ class SubmissionFormWidget(QWidget):
self.InfoItem: Form widget to hold name:value self.InfoItem: Form widget to hold name:value
""" """
if key not in self.ignore: 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 widget
return None return None
@@ -304,15 +316,19 @@ class SubmissionFormWidget(QWidget):
# self.reagents = self.prsr.sub['reagents'] # self.reagents = self.prsr.sub['reagents']
# case _: # case _:
# already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing] # 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] # already_have = [reagent for reagent in self.pyd.reagents if not reagent.missing]
names = list(set([item.type for item in already_have])) # names = list(set([item.type for item in already_have]))
# logger.debug(f"Already have: {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] # 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}") # # logger.debug(f"Missing: {reagents}")
self.reagents = already_have + reagents # self.pyd.reagents = already_have + reagents
# logger.debug(f"Reagents: {self.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}") # 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) self.report.add_result(report)
logger.debug(f"Outgoing report: {self.report.results}") logger.debug(f"Outgoing report: {self.report.results}")
@@ -391,15 +407,18 @@ class SubmissionFormWidget(QWidget):
""" """
logger.debug(f"\n\nBeginning Submission\n\n") logger.debug(f"\n\nBeginning Submission\n\n")
report = Report() 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(f"Submission: {pformat(self.pyd)}")
logger.debug("Checking kit integrity...") logger.debug("Checking kit integrity...")
result = self.pyd.check_kit_integrity() _, result = self.pyd.check_kit_integrity()
report.add_result(result) report.add_result(result)
if len(result.results) > 0: if len(result.results) > 0:
self.app.report.add_result(report) self.app.report.add_result(report)
self.app.result_reporter() self.app.result_reporter()
return return
logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n")
base_submission, result = self.pyd.toSQL() base_submission, result = self.pyd.toSQL()
# logger.debug(f"Base submission: {base_submission.to_dict()}") # logger.debug(f"Base submission: {base_submission.to_dict()}")
# check output message for issues # check output message for issues
@@ -465,6 +484,7 @@ class SubmissionFormWidget(QWidget):
Returns: Returns:
PydSubmission: Pydantic submission object PydSubmission: Pydantic submission object
""" """
report = Report()
logger.debug(f"Hello from form parser!") logger.debug(f"Hello from form parser!")
info = {} info = {}
reagents = [] reagents = []
@@ -481,6 +501,7 @@ class SubmissionFormWidget(QWidget):
info[field] = value info[field] = value
logger.debug(f"Info: {pformat(info)}") logger.debug(f"Info: {pformat(info)}")
logger.debug(f"Reagents: {pformat(reagents)}") 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()]}") # 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: for item in self.recover:
logger.debug(f"Attempting to recover: {item}") logger.debug(f"Attempting to recover: {item}")
@@ -488,8 +509,11 @@ class SubmissionFormWidget(QWidget):
value = getattr(self, item) value = getattr(self, item)
logger.debug(f"Setting {item}") logger.debug(f"Setting {item}")
info[item] = value info[item] = value
submission = PydSubmission(reagents=reagents, **info) # submission = PydSubmission(reagents=reagents, **info)
return submission for k,v in info.items():
self.pyd.set_attribute(key=k, value=v)
# return submission
self.report.add_result(report)
class InfoItem(QWidget): class InfoItem(QWidget):
@@ -497,7 +521,7 @@ class SubmissionFormWidget(QWidget):
super().__init__(parent) super().__init__(parent)
layout = QVBoxLayout() layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value) 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) self.setObjectName(key)
try: try:
self.missing:bool = value['missing'] self.missing:bool = value['missing']
@@ -684,8 +708,9 @@ class SubmissionFormWidget(QWidget):
Tuple[PydReagent, dict]: PydReagent and Report(?) Tuple[PydReagent, dict]: PydReagent and Report(?)
""" """
lot = self.lot.currentText() 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) 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: 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?") 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(): if dlg.exec():
@@ -764,19 +789,20 @@ class SubmissionFormWidget(QWidget):
looked_up_reg = None looked_up_reg = None
if isinstance(looked_up_reg, list): if isinstance(looked_up_reg, list):
looked_up_reg = None 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: if looked_up_reg != None:
relevant_reagents.remove(str(looked_up_reg.lot)) relevant_reagents.remove(str(looked_up_reg.lot))
relevant_reagents.insert(0, str(looked_up_reg.lot)) relevant_reagents.insert(0, str(looked_up_reg.lot))
else: else:
if len(relevant_reagents) > 1: 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)) 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) moved_reag = relevant_reagents.pop(idx)
relevant_reagents.insert(0, moved_reag) relevant_reagents.insert(0, moved_reag)
else: else:
logger.debug(f"Found {reagent.lot} in relevant reagents: {relevant_reagents}. But no need to move due to short list.") # 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}") pass
# logger.debug(f"New relevant reagents: {relevant_reagents}")
self.setObjectName(f"lot_{reagent.type}") self.setObjectName(f"lot_{reagent.type}")
self.addItems(relevant_reagents) self.addItems(relevant_reagents)

View File

@@ -14,7 +14,6 @@ from sqlalchemy import create_engine
from pydantic import field_validator, BaseModel, Field from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List from typing import Any, Tuple, Literal, List
from __init__ import bcolors
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -296,6 +295,17 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
class CustomFormatter(logging.Formatter): 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" format = "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s"
FORMATS = { FORMATS = {