From 1c89c31d257fcaa4cf62b4053bb649d78935a818 Mon Sep 17 00:00:00 2001 From: Landon Wark Date: Thu, 16 Feb 2023 15:30:38 -0600 Subject: [PATCH] Troubleshooting reports --- src/submissions/__init__.py | 4 +- src/submissions/backend/db/__init__.py | 55 +++++++++++++------ src/submissions/backend/db/models/controls.py | 2 +- .../backend/db/models/submissions.py | 10 ++++ src/submissions/backend/excel/parser.py | 8 ++- src/submissions/backend/excel/reports.py | 25 ++++++--- src/submissions/frontend/__init__.py | 10 +++- .../frontend/custom_widgets/sub_details.py | 52 +++++++++++++++--- .../templates/submission_details.html | 2 + .../templates/submission_details.txt | 4 +- src/submissions/tools/__init__.py | 14 ++++- 11 files changed, 146 insertions(+), 40 deletions(-) diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index 13844c3..190ce7a 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -1,4 +1,6 @@ # __init__.py # Version of the realpython-reader package -__version__ = "1.3.0" +__version__ = "202302.3b" +__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} +__copyright__ = "2022-2023, Government of Canada" diff --git a/src/submissions/backend/db/__init__.py b/src/submissions/backend/db/__init__.py index bd6a8b6..198f85e 100644 --- a/src/submissions/backend/db/__init__.py +++ b/src/submissions/backend/db/__init__.py @@ -15,7 +15,9 @@ import json # from dateutil.relativedelta import relativedelta from getpass import getuser import numpy as np -from tools import check_not_nan +from tools import check_not_nan, check_is_power_user +import yaml +from pathlib import Path logger = logging.getLogger(f"submissions.{__name__}") @@ -367,7 +369,8 @@ def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization: models.Organization: retrieved organization """ logger.debug(f"Querying organization: {name}") - return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first() + # return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first() + return ctx['database_session'].query(models.Organization).filter(models.Organization.name.startswith(name)).first() def submissions_to_df(ctx:dict, sub_type:str|None=None) -> pd.DataFrame: """ @@ -457,13 +460,10 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> dict: Returns: dict: a dictionary containing results of db addition """ - try: - power_users = ctx['power_users'] - except KeyError: - logger.debug("This user does not have permission to add kits.") - return {'code':1,'message':"This user does not have permission to add kits."} - logger.debug(f"Adding kit for user: {getuser()}") - if getuser() not in power_users: + # try: + # power_users = ctx['power_users'] + # except KeyError: + if not check_is_power_user(ctx=ctx): logger.debug(f"{getuser()} does not have permission to add kits.") return {'code':1, 'message':"This user does not have permission to add kits."} for type in exp: @@ -499,13 +499,14 @@ def create_org_from_yaml(ctx:dict, org:dict) -> dict: Returns: dict: dictionary containing results of db addition """ - try: - power_users = ctx['power_users'] - except KeyError: - logger.debug("This user does not have permission to add kits.") - return {'code':1,'message':"This user does not have permission to add organizations."} - logger.debug(f"Adding organization for user: {getuser()}") - if getuser() not in power_users: + # try: + # power_users = ctx['power_users'] + # except KeyError: + # logger.debug("This user does not have permission to add kits.") + # return {'code':1,'message':"This user does not have permission to add organizations."} + # logger.debug(f"Adding organization for user: {getuser()}") + # if getuser() not in power_users: + if not check_is_power_user(ctx=ctx): logger.debug(f"{getuser()} does not have permission to add kits.") return {'code':1, 'message':"This user does not have permission to add organizations."} for client in org: @@ -623,4 +624,24 @@ def lookup_submission_by_rsl_num(ctx:dict, rsl_num:str): def lookup_submissions_using_reagent(ctx:dict, reagent:models.Reagent) -> list[models.BasicSubmission]: - return ctx['database_session'].query(models.BasicSubmission).join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id).all() \ No newline at end of file + return ctx['database_session'].query(models.BasicSubmission).join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id).all() + + +def delete_submission_by_id(ctx:dict, id:int) -> None: + """ + Deletes a submission and its associated samples from the database. + + Args: + ctx (dict): settings passed down from gui + id (int): id of submission to be deleted. + """ + # In order to properly do this Im' going to have to delete all of the secondary table stuff as well. + sub = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first() + backup = sub.to_dict() + with open(Path(ctx['backup_path']).joinpath(f"{sub.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')}).yml"), "w") as f: + yaml.dump(backup, f) + sub.reagents = [] + for sample in sub.samples: + ctx['database_session'].delete(sample) + ctx["database_session"].delete(sub) + ctx["database_session"].commit() \ No newline at end of file diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 5e73a54..0d9c8f9 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -56,7 +56,7 @@ class Control(Base): output = { "name" : self.name, "type" : self.controltype.name, - "targets" : " ,".join(targets), + "targets" : ", ".join(targets), "kraken" : new_kraken[0:5] } return output diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 0daf05d..1ca5b0f 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -74,6 +74,14 @@ class BasicSubmission(Base): ext_info = json.loads(self.extraction_info) except TypeError: ext_info = None + try: + reagents = [item.to_sub_dict() for item in self.reagents] + except: + reagents = None + try: + samples = [item.to_sub_dict() for item in self.samples] + except: + samples = None output = { "id": self.id, "Plate Number": self.rsl_plate_num, @@ -85,6 +93,8 @@ class BasicSubmission(Base): "Extraction Kit": ext_kit, "Technician": self.technician, "Cost": self.run_cost, + "reagents": reagents, + "samples": samples, "ext_info": ext_info } # logger.debug(f"{self.rsl_plate_num} extraction: {output['Extraction Status']}") diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index bc1bded..0d4c029 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -187,14 +187,18 @@ class SheetParser(object): if not isinstance(row[5], float) and check_not_nan(row[5]): # must be prefixed with 'lot_' to be recognized by gui # regex below will remove 80% from 80% ethanol in the Wastewater kit. - output_key = re.sub(r"\d{1,3}%", "", row[0].lower().strip().replace(' ', '_')) + output_key = re.sub(r"^\d{1,3}%\s?", "", row[0].lower().strip().replace(' ', '_')) + output_key = output_key.strip("_") try: output_var = row[5].upper() except AttributeError: logger.debug(f"Couldn't upperize {row[5]}, must be a number") output_var = row[5] if check_not_nan(row[7]): - expiry = row[7].date() + try: + expiry = row[7].date() + except AttributeError: + expiry = date.today() else: expiry = date.today() self.sub[f"lot_{output_key}"] = {'lot':output_var, 'exp':expiry} diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 0ffd4a6..674e040 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -41,6 +41,9 @@ def make_report_xlsx(records:list[dict]) -> DataFrame: # df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format) return df2 +# def split_row_item(item:str) -> float: +# return item.split(" ")[-1] + def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str: @@ -59,17 +62,23 @@ def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str: output = [] logger.debug(f"Report DataFrame: {df}") for ii, row in enumerate(df.iterrows()): - row = [item for item in row] - logger.debug(f"Row: {row}") - + # row = [item for item in row] + logger.debug(f"Row {ii}: {row}") lab = row[0][0] + logger.debug(type(row)) logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") - kit = dict(name=row[0][1], cost=row[1]['Cost'], plate_count=int(row[1]['Kit Count']), sample_count=int(row[1]['Sample Count'])) + logger.debug(f"Name: {row[0][1]}") + data = [item for item in row[1]] + # logger.debug(data) + # logger.debug(f"Cost: {split_row_item(data[1])}") + # logger.debug(f"Kit count: {split_row_item(data[0])}") + # logger.debug(f"Sample Count: {split_row_item(data[2])}") + kit = dict(name=row[0][1], cost=data[1], plate_count=int(data[0]), sample_count=int(data[2])) if lab == old_lab: - output[ii-1]['kits'].append(kit) - output[ii-1]['total_cost'] += kit['cost'] - output[ii-1]['total_samples'] += kit['sample_count'] - output[ii-1]['total_plates'] += kit['plate_count'] + output[-1]['kits'].append(kit) + output[-1]['total_cost'] += kit['cost'] + output[-1]['total_samples'] += kit['sample_count'] + output[-1]['total_plates'] += kit['plate_count'] else: adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_plates=kit['plate_count']) output.append(adder) diff --git a/src/submissions/frontend/__init__.py b/src/submissions/frontend/__init__.py index a565fe9..fc259a5 100644 --- a/src/submissions/frontend/__init__.py +++ b/src/submissions/frontend/__init__.py @@ -52,7 +52,7 @@ class App(QMainWindow): super().__init__() self.ctx = ctx try: - self.title = f"Submissions App (v{ctx['package'].__version__})" + self.title = f"Submissions App (v{ctx['package'].__version__}) - {ctx['database']}" except AttributeError: self.title = f"Submissions App" # set initial app position and size @@ -85,6 +85,7 @@ class App(QMainWindow): reportMenu = menuBar.addMenu("&Reports") maintenanceMenu = menuBar.addMenu("&Monthly") helpMenu = menuBar.addMenu("&Help") + helpMenu.addAction(self.helpAction) fileMenu.addAction(self.importAction) reportMenu.addAction(self.generateReportAction) maintenanceMenu.addAction(self.joinControlsAction) @@ -111,6 +112,7 @@ class App(QMainWindow): self.addOrgAction = QAction("Add Org", self) self.joinControlsAction = QAction("Link Controls") self.joinExtractionAction = QAction("Link Ext Logs") + self.helpAction = QAction("&About", self) def _connectActions(self): @@ -128,6 +130,12 @@ class App(QMainWindow): self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter) self.joinControlsAction.triggered.connect(self.linkControls) self.joinExtractionAction.triggered.connect(self.linkExtractions) + self.helpAction.triggered.connect(self.showAbout) + + def showAbout(self): + output = f"Version: {self.ctx['package'].__version__}\n\nAuthor: {self.ctx['package'].__author__['name']} - {self.ctx['package'].__author__['email']}\n\nCopyright: {self.ctx['package'].__copyright__}" + about = AlertPop(message=output, status="information") + about.exec() def importSubmission(self): diff --git a/src/submissions/frontend/custom_widgets/sub_details.py b/src/submissions/frontend/custom_widgets/sub_details.py index a0e5247..9eea304 100644 --- a/src/submissions/frontend/custom_widgets/sub_details.py +++ b/src/submissions/frontend/custom_widgets/sub_details.py @@ -2,17 +2,19 @@ from datetime import date from PyQt6.QtWidgets import ( QVBoxLayout, QDialog, QTableView, QTextEdit, QPushButton, QScrollArea, - QMessageBox, QFileDialog + QMessageBox, QFileDialog, QMenu ) from PyQt6.QtCore import Qt, QAbstractTableModel -from PyQt6.QtGui import QFontMetrics +from PyQt6.QtGui import QFontMetrics, QAction, QCursor -from backend.db import submissions_to_df, lookup_submission_by_id, lookup_all_sample_types, create_kit_from_yaml +from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id from jinja2 import Environment, FileSystemLoader from xhtml2pdf import pisa import sys from pathlib import Path import logging +from .pop_ups import AlertPop, QuestionAsker +from tools import check_is_power_user logger = logging.getLogger(f"submissions.{__name__}") @@ -91,6 +93,14 @@ class SubmissionsSheet(QTableView): sets data in model """ self.data = submissions_to_df(ctx=self.ctx) + try: + del self.data['samples'] + except KeyError: + pass + try: + del self.data['reagents'] + except KeyError: + pass self.model = pandasModel(self.data) self.setModel(self.model) # self.resize(800,600) @@ -99,15 +109,43 @@ class SubmissionsSheet(QTableView): """ creates detailed data to show in seperate window """ - index=(self.selectionModel().currentIndex()) + index = (self.selectionModel().currentIndex()) # logger.debug(index) - value=index.sibling(index.row(),0).data() + value = index.sibling(index.row(),0).data() dlg = SubmissionDetails(ctx=self.ctx, id=value) # dlg.show() if dlg.exec(): pass + def contextMenuEvent(self, event): + self.menu = QMenu(self) + renameAction = QAction('Delete', self) + detailsAction = QAction('Details', self) + # Originally I intended to limit deletions to power users. + # renameAction.setEnabled(False) + # if check_is_power_user(ctx=self.ctx): + # renameAction.setEnabled(True) + renameAction.triggered.connect(lambda: self.delete_item(event)) + detailsAction.triggered.connect(lambda: self.show_details()) + self.menu.addAction(detailsAction) + self.menu.addAction(renameAction) + # add other required actions + self.menu.popup(QCursor.pos()) + + + def delete_item(self, event): + index = (self.selectionModel().currentIndex()) + value = index.sibling(index.row(),0).data() + logger.debug(index) + msg = QuestionAsker(title="Delete?", message=f"Are you sure you want to delete {index.sibling(index.row(),1).data()}?\n") + if msg.exec(): + delete_submission_by_id(ctx=self.ctx, id=value) + else: + return + self.setData() + + class SubmissionDetails(QDialog): @@ -130,8 +168,8 @@ class SubmissionDetails(QDialog): # don't want id del self.base_dict['id'] # convert sub objects to dicts - self.base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents] - self.base_dict['samples'] = [item.to_sub_dict() for item in data.samples] + # self.base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents] + # self.base_dict['samples'] = [item.to_sub_dict() for item in data.samples] # retrieve jinja template template = env.get_template("submission_details.txt") # render using object dict diff --git a/src/submissions/templates/submission_details.html b/src/submissions/templates/submission_details.html index 185f925..13d0967 100644 --- a/src/submissions/templates/submission_details.html +++ b/src/submissions/templates/submission_details.html @@ -20,6 +20,7 @@     {{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
{% endif %} {% endfor %}

+ {% if sub['samples'] %}

Samples:

{% for item in sub['samples'] %} {% if loop.index == 1 %} @@ -28,6 +29,7 @@     {{ item['well'] }}: {{ item['name'] }}
{% endif %} {% endfor %}

+ {% endif %} {% if sub['controls'] %}

Attached Controls:

{% for item in sub['controls'] %} diff --git a/src/submissions/templates/submission_details.txt b/src/submissions/templates/submission_details.txt index c0834c8..06c3a0a 100644 --- a/src/submissions/templates/submission_details.txt +++ b/src/submissions/templates/submission_details.txt @@ -6,10 +6,10 @@ Reagents: {% for item in sub['reagents'] %} {{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }}){% endfor %} - +{% if sub['samples']%} Samples: {% for item in sub['samples'] %} - {{ item['well'] }}: {{ item['name'] }}{% endfor %} + {{ item['well'] }}: {{ item['name'] }}{% endfor %}{% endif %} {% if sub['controls'] %} Attached Controls: {% for item in sub['controls'] %} diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index b46c972..049e125 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -1,5 +1,6 @@ import numpy as np import logging +import getpass logger = logging.getLogger(f"submissions.{__name__}") @@ -10,4 +11,15 @@ def check_not_nan(cell_contents) -> bool: return True except Exception as e: logger.debug(f"Check encounteded unknown error: {type(e).__name__} - {e}") - return False \ No newline at end of file + return False + + +def check_is_power_user(ctx:dict) -> bool: + try: + check = getpass.getuser() in ctx['power_users'] + except KeyError as e: + check = False + except Exception as e: + logger.debug(f"Check encounteded unknown error: {type(e).__name__} - {e}") + check = False + return check \ No newline at end of file