Pre-fixing JSON update bug.

This commit is contained in:
Landon Wark
2024-04-15 08:12:36 -05:00
parent f994f81d11
commit 7c46578d21
9 changed files with 90 additions and 40 deletions

View File

@@ -2,6 +2,7 @@
- Various bug fixes. - Various bug fixes.
- Move import PCR results to context menu. - Move import PCR results to context menu.
- Automated backup of database.
- Added ability to sign off on submission in submission details. - Added ability to sign off on submission in submission details.
## 202403.03 ## 202403.03

View File

@@ -1,5 +1,11 @@
- [ ] Appending of qPCR results to WW not saving. Find out why.
- Possibly due to immutable JSON? But... it's worked before... Right?
- Based on research, if a top-level JSON field is not changed, SQLalchemy will not detect changes.
- May have to use a special class: [link](https://docs.sqlalchemy.org/en/14/orm/extensions/mutable.html#establishing-mutability-on-scalar-column-values)
- [ ] Add Bead basher and Assit to DB.
- [x] Artic not creating right plate name.
- [ ] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass - [ ] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass
- [ ] Fix updating of Extraction Kit in submission form widget. - [x] 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

@@ -86,6 +86,7 @@ class BaseClass(Base):
""" """
Add the object to the database and commit Add the object to the database and commit
""" """
logger.debug(f"Saving object: {pformat(self.__dict__)}")
try: try:
self.__database_session__.add(self) self.__database_session__.add(self)
self.__database_session__.commit() self.__database_session__.commit()

View File

@@ -436,6 +436,7 @@ class BasicSubmission(BaseClass):
Args: Args:
original (bool, optional): Is this the first save. Defaults to True. original (bool, optional): Is this the first save. Defaults to True.
""" """
logger.debug("Saving submission.")
if original: if original:
self.uploaded_by = getuser() self.uploaded_by = getuser()
super().save() super().save()
@@ -605,12 +606,15 @@ class BasicSubmission(BaseClass):
# logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} Enforcer!") # logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} Enforcer!")
# return instr # return instr
from backend.validators import RSLNamer from backend.validators import RSLNamer
logger.debug(f"instr coming into {cls}: {instr}")
logger.debug(f"data coming into {cls}: {data}")
defaults = cls.get_default_info() defaults = cls.get_default_info()
data['abbreviation'] = defaults['abbreviation'] data['abbreviation'] = defaults['abbreviation']
if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]: if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]:
data['submission_type'] = defaults['submission_type'] data['submission_type'] = defaults['submission_type']
# outstr = super().enforce_name(instr=instr, data=data) # outstr = super().enforce_name(instr=instr, data=data)
if instr in [None, ""]: if instr in [None, ""]:
logger.debug("Sending to RSLNamer to make new plate name.")
outstr = RSLNamer.construct_new_plate_name(data=data) outstr = RSLNamer.construct_new_plate_name(data=data)
else: else:
outstr = instr outstr = instr
@@ -620,6 +624,7 @@ class BasicSubmission(BaseClass):
outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr) outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr)
outstr = re.sub(rf"{data['abbreviation']}(\d{6})", rf"{data['abbreviation']}-\1", outstr, flags=re.IGNORECASE).upper() outstr = re.sub(rf"{data['abbreviation']}(\d{6})", rf"{data['abbreviation']}-\1", outstr, flags=re.IGNORECASE).upper()
except (AttributeError, TypeError) as e: except (AttributeError, TypeError) as e:
logger.error(f"Error making outstr: {e}, sending to RSLNamer to make new plate name.")
outstr = RSLNamer.construct_new_plate_name(data=data) outstr = RSLNamer.construct_new_plate_name(data=data)
try: try:
plate_number = re.search(r"(?:(-|_)\d)(?!\d)", outstr).group().strip("_").strip("-") plate_number = re.search(r"(?:(-|_)\d)(?!\d)", outstr).group().strip("_").strip("-")
@@ -1281,28 +1286,32 @@ class Wastewater(BasicSubmission):
if hasattr(self, 'pcr_info') and self.pcr_info != None: if hasattr(self, 'pcr_info') and self.pcr_info != None:
# existing = json.loads(sub.pcr_info) # existing = json.loads(sub.pcr_info)
existing = self.pcr_info existing = self.pcr_info
logger.debug(f"Found existing pcr info: {pformat(self.pcr_info)}")
else: else:
existing = None existing = None
if existing != None: if existing != None:
# update pcr_info # update pcr_info
try: try:
logger.debug(f"Updating {type(existing)}: {existing} with {type(parser.pcr)}: {parser.pcr}") logger.debug(f"Updating {type(existing)}:\n {pformat(existing)} with {type(parser.pcr)}:\n {pformat(parser.pcr)}")
# if json.dumps(parser.pcr) not in sub.pcr_info: # if json.dumps(parser.pcr) not in sub.pcr_info:
if parser.pcr not in self.pcr_info: if parser.pcr not in self.pcr_info:
logger.debug(f"This is new pcr info, appending to existing")
existing.append(parser.pcr) existing.append(parser.pcr)
logger.debug(f"Setting: {existing}") else:
logger.debug("This info already exists, skipping.")
# logger.debug(f"Setting {self.rsl_plate_num} PCR to:\n {pformat(existing)}")
# sub.pcr_info = json.dumps(existing) # sub.pcr_info = json.dumps(existing)
self.pcr_info = existing self.pcr_info = existing
except TypeError: except TypeError:
logger.error(f"Error updating!") logger.error(f"Error updating!")
# sub.pcr_info = json.dumps([parser.pcr]) # sub.pcr_info = json.dumps([parser.pcr])
self.pcr_info = [parser.pcr] self.pcr_info = [parser.pcr]
logger.debug(f"Final pcr info for {self.rsl_plate_num}: {self.pcr_info}") logger.debug(f"Final pcr info for {self.rsl_plate_num}:\n {pformat(self.pcr_info)}")
else: else:
# sub.pcr_info = json.dumps([parser.pcr]) # sub.pcr_info = json.dumps([parser.pcr])
self.pcr_info = [parser.pcr] self.pcr_info = [parser.pcr]
logger.debug(f"Existing {type(self.pcr_info)}: {self.pcr_info}") # logger.debug(f"Existing {type(self.pcr_info)}: {self.pcr_info}")
logger.debug(f"Inserting {type(parser.pcr)}: {parser.pcr}") # logger.debug(f"Inserting {type(parser.pcr)}: {parser.pcr}")
self.save(original=False) self.save(original=False)
logger.debug(f"Got {len(parser.samples)} samples to update!") logger.debug(f"Got {len(parser.samples)} samples to update!")
logger.debug(f"Parser samples: {parser.samples}") logger.debug(f"Parser samples: {parser.samples}")
@@ -1383,15 +1392,15 @@ class WastewaterArtic(BasicSubmission):
instr = re.sub(r"Artic", "", instr, flags=re.IGNORECASE) instr = re.sub(r"Artic", "", instr, flags=re.IGNORECASE)
except (AttributeError, TypeError) as e: except (AttributeError, TypeError) as e:
logger.error(f"Problem using regex: {e}") logger.error(f"Problem using regex: {e}")
try: # try:
check = instr.startswith("RSL") # check = instr.startswith("RSL")
except AttributeError: # except AttributeError:
check = False # check = False
if not check: # if not check:
try: # try:
instr = "RSL" + instr # instr = "RSL" + instr
except: # except TypeError:
instr = "RSL" # instr = "RSL"
outstr = super().enforce_name(instr=instr, data=data) outstr = super().enforce_name(instr=instr, data=data)
return outstr return outstr
@@ -1540,7 +1549,8 @@ class WastewaterArtic(BasicSubmission):
df.sort_values(by=['destination_column', 'destination_row'], inplace=True) df.sort_values(by=['destination_column', 'destination_row'], inplace=True)
except AttributeError as e: except AttributeError as e:
logger.error(f"Couldn't construct df due to {e}") logger.error(f"Couldn't construct df due to {e}")
input_dict['csv'] = df # input_dict['csv'] = df
input_dict['csv'] = xl.parse("hitpicks_csv_to_export")
return input_dict return input_dict
@classmethod @classmethod

View File

@@ -266,7 +266,11 @@ class ReagentParser(object):
# logger.debug(f"Got lot for {item}-{name}: {lot} as {type(lot)}") # logger.debug(f"Got lot for {item}-{name}: {lot} as {type(lot)}")
lot = str(lot) lot = str(lot)
logger.debug(f"Going into pydantic: name: {name}, lot: {lot}, expiry: {expiry}, type: {item.strip()}, comment: {comment}") logger.debug(f"Going into pydantic: name: {name}, lot: {lot}, expiry: {expiry}, type: {item.strip()}, comment: {comment}")
if name.lower() != "not applicable": try:
check = name.lower() != "not applicable"
except AttributeError:
check = True
if check:
listo.append(PydReagent(type=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, missing=missing)) listo.append(PydReagent(type=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, missing=missing))
return listo return listo

View File

@@ -192,7 +192,7 @@ class PydSample(BaseModel, extra='allow'):
row=row, column=column, id=id) row=row, column=column, id=id)
# logger.debug(f"Using submission_sample_association: {association}") # logger.debug(f"Using submission_sample_association: {association}")
try: try:
instance.sample_submission_associations.append(association) # instance.sample_submission_associations.append(association)
out_associations.append(association) out_associations.append(association)
except IntegrityError as e: except IntegrityError as e:
logger.error(f"Could not attach submission sample association due to: {e}") logger.error(f"Could not attach submission sample association due to: {e}")
@@ -809,7 +809,8 @@ class PydSubmission(BaseModel, extra='allow'):
# # 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 = [] missing = []
output_reagents = self.reagents # output_reagents = self.reagents
output_reagents = ext_kit_rtypes
logger.debug(f"Already have these reagent types: {reagenttypes}") logger.debug(f"Already have these reagent types: {reagenttypes}")
for rt in ext_kit_rtypes: for rt in ext_kit_rtypes:
if rt.type not in reagenttypes: if rt.type not in reagenttypes:

View File

@@ -9,9 +9,10 @@ from PyQt6.QtWidgets import (
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from tools import check_if_app, Settings, Report from tools import check_if_app, Settings, Report
from datetime import date
from .pop_ups import AlertPop from .pop_ups import AlertPop
from .misc import LogParser from .misc import LogParser
import logging, webbrowser, sys import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
@@ -51,6 +52,7 @@ class App(QMainWindow):
self._connectActions() self._connectActions()
self.show() self.show()
self.statusBar().showMessage('Ready', 5000) self.statusBar().showMessage('Ready', 5000)
self.backup_database()
def _createMenuBar(self): def _createMenuBar(self):
""" """
@@ -159,6 +161,16 @@ class App(QMainWindow):
dlg = LogParser(self) dlg = LogParser(self)
dlg.exec() dlg.exec()
def backup_database(self):
month = date.today().strftime("%Y-%m")
# day = date.today().strftime("%Y-%m-%d")
logger.debug(f"Here is the db directory: {self.ctx.database_path}")
logger.debug(f"Here is the backup directory: {self.ctx.backup_path}")
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db")
if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__():
logger.debug("No backup found for this month, backing up database.")
shutil.copyfile(self.ctx.database_path, current_month_bak)
class AddSubForm(QWidget): class AddSubForm(QWidget):
def __init__(self, parent:QWidget): def __init__(self, parent:QWidget):

View File

@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import pyqtSignal from PyQt6.QtCore import pyqtSignal
from pathlib import Path from pathlib import Path
from . import select_open_file, select_save_file from . import select_open_file, select_save_file
import logging, difflib, inspect, json import logging, difflib, inspect, pickle
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan from tools import Report, Result, check_not_nan
from backend.excel.parser import SheetParser, PCRParser from backend.excel.parser import SheetParser, PCRParser
@@ -147,7 +147,7 @@ class SubmissionFormWidget(QWidget):
def __init__(self, parent: QWidget, submission:PydSubmission) -> 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.pyd = submission
# self.input = [{k:v} for k,v in kwargs.items()] # self.input = [{k:v} for k,v in kwargs.items()]
@@ -177,18 +177,18 @@ class SubmissionFormWidget(QWidget):
self.scrape_reagents(self.pyd.extraction_kit) 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.pyd, "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)
export_csv_btn.clicked.connect(self.export_csv_function) # export_csv_btn.clicked.connect(self.export_csv_function)
submit_btn = QPushButton("Submit") # submit_btn = QPushButton("Submit")
submit_btn.setObjectName("submit_btn") # submit_btn.setObjectName("submit_btn")
self.layout.addWidget(submit_btn) # self.layout.addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample_function) # submit_btn.clicked.connect(self.submit_new_sample_function)
self.setLayout(self.layout) # self.setLayout(self.layout)
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|PydReagent, submission_type:str|None=None, extraction_kit: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":
""" """
@@ -230,7 +230,7 @@ class SubmissionFormWidget(QWidget):
caller = inspect.stack()[1].function.__repr__().replace("'", "") caller = inspect.stack()[1].function.__repr__().replace("'", "")
# self.reagents = [] # self.reagents = []
# logger.debug(f"Self.reagents: {self.reagents}") # logger.debug(f"Self.reagents: {self.reagents}")
# logger.debug(f"\n\n{caller}\n\n") logger.debug(f"\n\n{pformat(caller)}\n\n")
# logger.debug(f"SubmissionType: {self.submission_type}") # logger.debug(f"SubmissionType: {self.submission_type}")
report = Report() report = Report()
logger.debug(f"Extraction kit: {extraction_kit}") logger.debug(f"Extraction kit: {extraction_kit}")
@@ -257,13 +257,25 @@ class SubmissionFormWidget(QWidget):
# self.pyd.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) reagents, integrity_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: for reagent in reagents:
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit) add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit)
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
self.report.add_result(report) report.add_result(integrity_report)
logger.debug(f"Outgoing report: {self.report.results}") logger.debug(f"Outgoing report: {report.results}")
if hasattr(self.pyd, "csv"):
export_csv_btn = QPushButton("Export CSV")
export_csv_btn.setObjectName("export_csv_btn")
self.layout.addWidget(export_csv_btn)
export_csv_btn.clicked.connect(self.export_csv_function)
submit_btn = QPushButton("Submit")
submit_btn.setObjectName("submit_btn")
self.layout.addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample_function)
self.setLayout(self.layout)
self.app.report.add_result(report)
self.app.result_reporter()
def kit_integrity_completion_function(self, extraction_kit:str|None=None): def kit_integrity_completion_function(self, extraction_kit:str|None=None):
""" """
@@ -302,7 +314,6 @@ class SubmissionFormWidget(QWidget):
Alternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents. Alternatively, 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!""".replace(" ", ""), status="Warning") \n\nPlease make sure you check the lots carefully!""".replace(" ", ""), status="Warning")
report.add_result(result) report.add_result(result)
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}")

View File

@@ -538,7 +538,11 @@ def rreplace(s, old, new):
ctx = get_config(None) ctx = get_config(None)
def is_power_user() -> bool: def is_power_user() -> bool:
return getpass.getuser() in ctx.power_users try:
check = getpass.getuser() in ctx.power_users
except:
check = False
return check
def check_authorization(func): def check_authorization(func):
""" """