From 7c46578d21c6e332d062082c13616dff10718c8a Mon Sep 17 00:00:00 2001 From: Landon Wark Date: Mon, 15 Apr 2024 08:12:36 -0500 Subject: [PATCH] Pre-fixing JSON update bug. --- CHANGELOG.md | 1 + TODO.md | 8 ++- src/submissions/backend/db/models/__init__.py | 1 + .../backend/db/models/submissions.py | 40 +++++++++------ src/submissions/backend/excel/parser.py | 6 ++- src/submissions/backend/validators/pydant.py | 5 +- src/submissions/frontend/widgets/app.py | 14 +++++- .../frontend/widgets/submission_widget.py | 49 ++++++++++++------- src/submissions/tools.py | 6 ++- 9 files changed, 90 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2b581..ff693aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Various bug fixes. - Move import PCR results to context menu. +- Automated backup of database. - Added ability to sign off on submission in submission details. ## 202403.03 diff --git a/TODO.md b/TODO.md index 3e79877..95fb75d 100644 --- a/TODO.md +++ b/TODO.md @@ -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 -- [ ] 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. - [ ] Create Tips ... *sigh*. - [x] Create platemap image from html for export to pdf. diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 7c6fade..3a46551 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -86,6 +86,7 @@ class BaseClass(Base): """ Add the object to the database and commit """ + logger.debug(f"Saving object: {pformat(self.__dict__)}") try: self.__database_session__.add(self) self.__database_session__.commit() diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index d6002d6..fd6018b 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -436,6 +436,7 @@ class BasicSubmission(BaseClass): Args: original (bool, optional): Is this the first save. Defaults to True. """ + logger.debug("Saving submission.") if original: self.uploaded_by = getuser() super().save() @@ -605,12 +606,15 @@ class BasicSubmission(BaseClass): # logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} Enforcer!") # return instr 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() data['abbreviation'] = defaults['abbreviation'] if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]: data['submission_type'] = defaults['submission_type'] # outstr = super().enforce_name(instr=instr, data=data) if instr in [None, ""]: + logger.debug("Sending to RSLNamer to make new plate name.") outstr = RSLNamer.construct_new_plate_name(data=data) else: 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(rf"{data['abbreviation']}(\d{6})", rf"{data['abbreviation']}-\1", outstr, flags=re.IGNORECASE).upper() 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) try: 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: # existing = json.loads(sub.pcr_info) existing = self.pcr_info + logger.debug(f"Found existing pcr info: {pformat(self.pcr_info)}") else: existing = None if existing != None: # update pcr_info 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 parser.pcr not in self.pcr_info: + logger.debug(f"This is new pcr info, appending to existing") 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) self.pcr_info = existing except TypeError: logger.error(f"Error updating!") # sub.pcr_info = json.dumps([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: # sub.pcr_info = json.dumps([parser.pcr]) self.pcr_info = [parser.pcr] - logger.debug(f"Existing {type(self.pcr_info)}: {self.pcr_info}") - logger.debug(f"Inserting {type(parser.pcr)}: {parser.pcr}") + # logger.debug(f"Existing {type(self.pcr_info)}: {self.pcr_info}") + # logger.debug(f"Inserting {type(parser.pcr)}: {parser.pcr}") self.save(original=False) logger.debug(f"Got {len(parser.samples)} samples to update!") logger.debug(f"Parser samples: {parser.samples}") @@ -1383,15 +1392,15 @@ class WastewaterArtic(BasicSubmission): instr = re.sub(r"Artic", "", instr, flags=re.IGNORECASE) except (AttributeError, TypeError) as e: logger.error(f"Problem using regex: {e}") - try: - check = instr.startswith("RSL") - except AttributeError: - check = False - if not check: - try: - instr = "RSL" + instr - except: - instr = "RSL" + # try: + # check = instr.startswith("RSL") + # except AttributeError: + # check = False + # if not check: + # try: + # instr = "RSL" + instr + # except TypeError: + # instr = "RSL" outstr = super().enforce_name(instr=instr, data=data) return outstr @@ -1540,7 +1549,8 @@ class WastewaterArtic(BasicSubmission): df.sort_values(by=['destination_column', 'destination_row'], inplace=True) except AttributeError as 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 @classmethod diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index 1c06cc9..7c9f2d0 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -266,7 +266,11 @@ class ReagentParser(object): # logger.debug(f"Got lot for {item}-{name}: {lot} as {type(lot)}") lot = str(lot) 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)) return listo diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index e42d5a1..f629c75 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -192,7 +192,7 @@ class PydSample(BaseModel, extra='allow'): row=row, column=column, id=id) # logger.debug(f"Using submission_sample_association: {association}") try: - instance.sample_submission_associations.append(association) + # instance.sample_submission_associations.append(association) out_associations.append(association) except IntegrityError as 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? # missing = list(set(ext_kit_rtypes).difference(reagenttypes)) missing = [] - output_reagents = self.reagents + # output_reagents = self.reagents + output_reagents = ext_kit_rtypes logger.debug(f"Already have these reagent types: {reagenttypes}") for rt in ext_kit_rtypes: if rt.type not in reagenttypes: diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index e403fb1..17118d4 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -9,9 +9,10 @@ from PyQt6.QtWidgets import ( from PyQt6.QtGui import QAction from pathlib import Path from tools import check_if_app, Settings, Report +from datetime import date from .pop_ups import AlertPop from .misc import LogParser -import logging, webbrowser, sys +import logging, webbrowser, sys, shutil from .submission_table import SubmissionsSheet from .submission_widget import SubmissionFormContainer from .controls_chart import ControlsViewer @@ -51,6 +52,7 @@ class App(QMainWindow): self._connectActions() self.show() self.statusBar().showMessage('Ready', 5000) + self.backup_database() def _createMenuBar(self): """ @@ -159,6 +161,16 @@ class App(QMainWindow): dlg = LogParser(self) 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): def __init__(self, parent:QWidget): diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 3db65c7..b375314 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -5,7 +5,7 @@ from PyQt6.QtWidgets import ( from PyQt6.QtCore import pyqtSignal from pathlib import Path from . import select_open_file, select_save_file -import logging, difflib, inspect, json +import logging, difflib, inspect, pickle from pathlib import Path from tools import Report, Result, check_not_nan from backend.excel.parser import SheetParser, PCRParser @@ -147,7 +147,7 @@ class SubmissionFormWidget(QWidget): def __init__(self, parent: QWidget, submission:PydSubmission) -> None: super().__init__(parent) - self.report = Report() + # self.report = Report() self.app = parent.app self.pyd = submission # self.input = [{k:v} for k,v in kwargs.items()] @@ -177,18 +177,18 @@ class SubmissionFormWidget(QWidget): 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.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(self.report) - self.app.result_reporter() + # 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(self.report) + # 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": """ @@ -230,7 +230,7 @@ class SubmissionFormWidget(QWidget): caller = inspect.stack()[1].function.__repr__().replace("'", "") # 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}") report = Report() logger.debug(f"Extraction kit: {extraction_kit}") @@ -257,13 +257,25 @@ class SubmissionFormWidget(QWidget): # self.pyd.reagents = already_have + reagents # logger.debug(f"Reagents: {self.reagents}") # 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}") 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}") + report.add_result(integrity_report) + 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): """ @@ -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. \n\nPlease make sure you check the lots carefully!""".replace(" ", ""), status="Warning") report.add_result(result) - self.report.add_result(report) logger.debug(f"Outgoing report: {self.report.results}") diff --git a/src/submissions/tools.py b/src/submissions/tools.py index 619af5f..8bbb35d 100644 --- a/src/submissions/tools.py +++ b/src/submissions/tools.py @@ -538,7 +538,11 @@ def rreplace(s, old, new): ctx = get_config(None) 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): """