Troubleshooting reports

This commit is contained in:
Landon Wark
2023-02-16 15:30:38 -06:00
parent 85dad791ec
commit 1c89c31d25
11 changed files with 146 additions and 40 deletions

View File

@@ -1,4 +1,6 @@
# __init__.py # __init__.py
# Version of the realpython-reader package # 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"

View File

@@ -15,7 +15,9 @@ import json
# from dateutil.relativedelta import relativedelta # from dateutil.relativedelta import relativedelta
from getpass import getuser from getpass import getuser
import numpy as np 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__}") 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 models.Organization: retrieved organization
""" """
logger.debug(f"Querying organization: {name}") 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: 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: Returns:
dict: a dictionary containing results of db addition dict: a dictionary containing results of db addition
""" """
try: # try:
power_users = ctx['power_users'] # power_users = ctx['power_users']
except KeyError: # except KeyError:
logger.debug("This user does not have permission to add kits.") if not check_is_power_user(ctx=ctx):
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:
logger.debug(f"{getuser()} does not have permission to add kits.") logger.debug(f"{getuser()} does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add kits."} return {'code':1, 'message':"This user does not have permission to add kits."}
for type in exp: for type in exp:
@@ -499,13 +499,14 @@ def create_org_from_yaml(ctx:dict, org:dict) -> dict:
Returns: Returns:
dict: dictionary containing results of db addition dict: dictionary containing results of db addition
""" """
try: # try:
power_users = ctx['power_users'] # power_users = ctx['power_users']
except KeyError: # except KeyError:
logger.debug("This user does not have permission to add kits.") # logger.debug("This user does not have permission to add kits.")
return {'code':1,'message':"This user does not have permission to add organizations."} # return {'code':1,'message':"This user does not have permission to add organizations."}
logger.debug(f"Adding organization for user: {getuser()}") # logger.debug(f"Adding organization for user: {getuser()}")
if getuser() not in power_users: # 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.") logger.debug(f"{getuser()} does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add organizations."} return {'code':1, 'message':"This user does not have permission to add organizations."}
for client in org: for client in org:
@@ -624,3 +625,23 @@ def lookup_submission_by_rsl_num(ctx:dict, rsl_num:str):
def lookup_submissions_using_reagent(ctx:dict, reagent:models.Reagent) -> list[models.BasicSubmission]: 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() 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()

View File

@@ -74,6 +74,14 @@ class BasicSubmission(Base):
ext_info = json.loads(self.extraction_info) ext_info = json.loads(self.extraction_info)
except TypeError: except TypeError:
ext_info = None 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 = { output = {
"id": self.id, "id": self.id,
"Plate Number": self.rsl_plate_num, "Plate Number": self.rsl_plate_num,
@@ -85,6 +93,8 @@ class BasicSubmission(Base):
"Extraction Kit": ext_kit, "Extraction Kit": ext_kit,
"Technician": self.technician, "Technician": self.technician,
"Cost": self.run_cost, "Cost": self.run_cost,
"reagents": reagents,
"samples": samples,
"ext_info": ext_info "ext_info": ext_info
} }
# logger.debug(f"{self.rsl_plate_num} extraction: {output['Extraction Status']}") # logger.debug(f"{self.rsl_plate_num} extraction: {output['Extraction Status']}")

View File

@@ -187,14 +187,18 @@ class SheetParser(object):
if not isinstance(row[5], float) and check_not_nan(row[5]): if not isinstance(row[5], float) and check_not_nan(row[5]):
# must be prefixed with 'lot_' to be recognized by gui # must be prefixed with 'lot_' to be recognized by gui
# regex below will remove 80% from 80% ethanol in the Wastewater kit. # 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: try:
output_var = row[5].upper() output_var = row[5].upper()
except AttributeError: except AttributeError:
logger.debug(f"Couldn't upperize {row[5]}, must be a number") logger.debug(f"Couldn't upperize {row[5]}, must be a number")
output_var = row[5] output_var = row[5]
if check_not_nan(row[7]): if check_not_nan(row[7]):
try:
expiry = row[7].date() expiry = row[7].date()
except AttributeError:
expiry = date.today()
else: else:
expiry = date.today() expiry = date.today()
self.sub[f"lot_{output_key}"] = {'lot':output_var, 'exp':expiry} self.sub[f"lot_{output_key}"] = {'lot':output_var, 'exp':expiry}

View File

@@ -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) # 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 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: 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 = [] output = []
logger.debug(f"Report DataFrame: {df}") logger.debug(f"Report DataFrame: {df}")
for ii, row in enumerate(df.iterrows()): for ii, row in enumerate(df.iterrows()):
row = [item for item in row] # row = [item for item in row]
logger.debug(f"Row: {row}") logger.debug(f"Row {ii}: {row}")
lab = row[0][0] lab = row[0][0]
logger.debug(type(row))
logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") 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: if lab == old_lab:
output[ii-1]['kits'].append(kit) output[-1]['kits'].append(kit)
output[ii-1]['total_cost'] += kit['cost'] output[-1]['total_cost'] += kit['cost']
output[ii-1]['total_samples'] += kit['sample_count'] output[-1]['total_samples'] += kit['sample_count']
output[ii-1]['total_plates'] += kit['plate_count'] output[-1]['total_plates'] += kit['plate_count']
else: else:
adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_plates=kit['plate_count']) adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_plates=kit['plate_count'])
output.append(adder) output.append(adder)

View File

@@ -52,7 +52,7 @@ class App(QMainWindow):
super().__init__() super().__init__()
self.ctx = ctx self.ctx = ctx
try: try:
self.title = f"Submissions App (v{ctx['package'].__version__})" self.title = f"Submissions App (v{ctx['package'].__version__}) - {ctx['database']}"
except AttributeError: except AttributeError:
self.title = f"Submissions App" self.title = f"Submissions App"
# set initial app position and size # set initial app position and size
@@ -85,6 +85,7 @@ class App(QMainWindow):
reportMenu = menuBar.addMenu("&Reports") reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly") maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help") helpMenu = menuBar.addMenu("&Help")
helpMenu.addAction(self.helpAction)
fileMenu.addAction(self.importAction) fileMenu.addAction(self.importAction)
reportMenu.addAction(self.generateReportAction) reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinControlsAction) maintenanceMenu.addAction(self.joinControlsAction)
@@ -111,6 +112,7 @@ class App(QMainWindow):
self.addOrgAction = QAction("Add Org", self) self.addOrgAction = QAction("Add Org", self)
self.joinControlsAction = QAction("Link Controls") self.joinControlsAction = QAction("Link Controls")
self.joinExtractionAction = QAction("Link Ext Logs") self.joinExtractionAction = QAction("Link Ext Logs")
self.helpAction = QAction("&About", self)
def _connectActions(self): def _connectActions(self):
@@ -128,6 +130,12 @@ class App(QMainWindow):
self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter) self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
self.joinControlsAction.triggered.connect(self.linkControls) self.joinControlsAction.triggered.connect(self.linkControls)
self.joinExtractionAction.triggered.connect(self.linkExtractions) 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): def importSubmission(self):

View File

@@ -2,17 +2,19 @@ from datetime import date
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView, QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea, QTextEdit, QPushButton, QScrollArea,
QMessageBox, QFileDialog QMessageBox, QFileDialog, QMenu
) )
from PyQt6.QtCore import Qt, QAbstractTableModel 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 jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa from xhtml2pdf import pisa
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
from .pop_ups import AlertPop, QuestionAsker
from tools import check_is_power_user
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -91,6 +93,14 @@ class SubmissionsSheet(QTableView):
sets data in model sets data in model
""" """
self.data = submissions_to_df(ctx=self.ctx) 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.model = pandasModel(self.data)
self.setModel(self.model) self.setModel(self.model)
# self.resize(800,600) # self.resize(800,600)
@@ -108,6 +118,34 @@ class SubmissionsSheet(QTableView):
pass 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): class SubmissionDetails(QDialog):
@@ -130,8 +168,8 @@ class SubmissionDetails(QDialog):
# don't want id # don't want id
del self.base_dict['id'] del self.base_dict['id']
# convert sub objects to dicts # convert sub objects to dicts
self.base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents] # 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['samples'] = [item.to_sub_dict() for item in data.samples]
# retrieve jinja template # retrieve jinja template
template = env.get_template("submission_details.txt") template = env.get_template("submission_details.txt")
# render using object dict # render using object dict

View File

@@ -20,6 +20,7 @@
&nbsp;&nbsp;&nbsp;&nbsp;{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br> &nbsp;&nbsp;&nbsp;&nbsp;{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
{% endif %} {% endif %}
{% endfor %}</p> {% endfor %}</p>
{% if sub['samples'] %}
<h3><u>Samples:</u></h3> <h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %} <p>{% for item in sub['samples'] %}
{% if loop.index == 1 %} {% if loop.index == 1 %}
@@ -28,6 +29,7 @@
&nbsp;&nbsp;&nbsp;&nbsp;{{ item['well'] }}: {{ item['name'] }}<br> &nbsp;&nbsp;&nbsp;&nbsp;{{ item['well'] }}: {{ item['name'] }}<br>
{% endif %} {% endif %}
{% endfor %}</p> {% endfor %}</p>
{% endif %}
{% if sub['controls'] %} {% if sub['controls'] %}
<h3><u>Attached Controls:</u></h3> <h3><u>Attached Controls:</u></h3>
{% for item in sub['controls'] %} {% for item in sub['controls'] %}

View File

@@ -6,10 +6,10 @@
Reagents: Reagents:
{% for item in sub['reagents'] %} {% for item in sub['reagents'] %}
{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }}){% endfor %} {{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }}){% endfor %}
{% if sub['samples']%}
Samples: Samples:
{% for item in sub['samples'] %} {% for item in sub['samples'] %}
{{ item['well'] }}: {{ item['name'] }}{% endfor %} {{ item['well'] }}: {{ item['name'] }}{% endfor %}{% endif %}
{% if sub['controls'] %} {% if sub['controls'] %}
Attached Controls: Attached Controls:
{% for item in sub['controls'] %} {% for item in sub['controls'] %}

View File

@@ -1,5 +1,6 @@
import numpy as np import numpy as np
import logging import logging
import getpass
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -11,3 +12,14 @@ def check_not_nan(cell_contents) -> bool:
except Exception as e: except Exception as e:
logger.debug(f"Check encounteded unknown error: {type(e).__name__} - {e}") logger.debug(f"Check encounteded unknown error: {type(e).__name__} - {e}")
return False 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