added kraken data to details.

This commit is contained in:
Landon Wark
2023-02-06 13:39:35 -06:00
parent 7fb5bb12f3
commit 963ac7d4a4
11 changed files with 177 additions and 24 deletions

View File

@@ -0,0 +1,32 @@
"""added user tracking
Revision ID: 785bb1140878
Revises: 178203610c3b
Create Date: 2023-02-06 09:54:20.371117
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '785bb1140878'
down_revision = '178203610c3b'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('uploaded_by', sa.String(length=32), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissions', schema=None) as batch_op:
batch_op.drop_column('uploaded_by')
# ### end Alembic commands ###

View File

@@ -1,4 +1,4 @@
# __init__.py # __init__.py
# Version of the realpython-reader package # Version of the realpython-reader package
__version__ = "1.2.1" __version__ = "1.2.2"

View File

@@ -341,6 +341,12 @@ def submissions_to_df(ctx:dict, type:str|None=None) -> pd.DataFrame:
# pass to lookup function # pass to lookup function
subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)] subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)]
df = pd.DataFrame.from_records(subs) df = pd.DataFrame.from_records(subs)
# logger.debug(f"Pre: {df['Technician']}")
try:
df = df.drop("controls", axis=1)
except:
logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.")
# logger.debug(f"Post: {df['Technician']}")
return df return df

View File

@@ -1,6 +1,11 @@
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
import logging
from operator import itemgetter
import json
logger = logging.getLogger(f"submissions.{__name__}")
class ControlType(Base): class ControlType(Base):
""" """
@@ -35,3 +40,24 @@ class Control(Base):
submission_id = Column(INTEGER, ForeignKey("_submissions.id")) #: parent submission id submission_id = Column(INTEGER, ForeignKey("_submissions.id")) #: parent submission id
submission = relationship("BacterialCulture", back_populates="controls", foreign_keys=[submission_id]) #: parent submission submission = relationship("BacterialCulture", back_populates="controls", foreign_keys=[submission_id]) #: parent submission
def to_sub_dict(self):
kraken = json.loads(self.kraken)
kraken_cnt_total = sum([kraken[item]['kraken_count'] for item in kraken])
new_kraken = []
for item in kraken:
kraken_percent = kraken[item]['kraken_count'] / kraken_cnt_total
new_kraken.append({'name': item, 'kraken_count':kraken[item]['kraken_count'], 'kraken_percent':"{0:.0%}".format(kraken_percent)})
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
if self.controltype.targets == []:
targets = ["None"]
else:
targets = self.controltype.targets
output = {
"name" : self.name,
"type" : self.controltype.name,
"targets" : " ,".join(targets),
"kraken" : new_kraken[0:5]
}
return output

View File

@@ -2,6 +2,9 @@ from . import Base
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime as dt from datetime import datetime as dt
import logging
logger = logging.getLogger(f"submissions.{__name__}")
# table containing reagents/submission relationships # table containing reagents/submission relationships
reagents_submissions = Table("_reagents_submissions", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("submission_id", INTEGER, ForeignKey("_submissions.id"))) reagents_submissions = Table("_reagents_submissions", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("submission_id", INTEGER, ForeignKey("_submissions.id")))
@@ -28,6 +31,7 @@ class BasicSubmission(Base):
reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents
extraction_info = Column(JSON) #: unstructured output from the extraction table logger. extraction_info = Column(JSON) #: unstructured output from the extraction table logger.
run_cost = Column(FLOAT(2)) run_cost = Column(FLOAT(2))
uploaded_by = Column(String(32))
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "basic_submission", "polymorphic_identity": "basic_submission",
@@ -77,6 +81,7 @@ class BasicSubmission(Base):
"Technician": self.technician, "Technician": self.technician,
"Cost": self.run_cost, "Cost": self.run_cost,
} }
logger.debug(f"{self.rsl_plate_num} technician: {output['Technician']}")
return output return output
@@ -107,6 +112,7 @@ class BasicSubmission(Base):
# cost = self.extraction_kit.cost_per_run # cost = self.extraction_kit.cost_per_run
# except AttributeError: # except AttributeError:
# cost = None # cost = None
output = { output = {
"id": self.id, "id": self.id,
"Plate Number": self.rsl_plate_num, "Plate Number": self.rsl_plate_num,
@@ -131,6 +137,13 @@ class BacterialCulture(BasicSubmission):
samples = relationship("BCSample", back_populates="rsl_plate", uselist=True) samples = relationship("BCSample", back_populates="rsl_plate", uselist=True)
# bc_sample_id = Column(INTEGER, ForeignKey("_bc_samples.id", ondelete="SET NULL", name="fk_BC_sample_id")) # bc_sample_id = Column(INTEGER, ForeignKey("_bc_samples.id", ondelete="SET NULL", name="fk_BC_sample_id"))
__mapper_args__ = {"polymorphic_identity": "bacterial_culture", "polymorphic_load": "inline"} __mapper_args__ = {"polymorphic_identity": "bacterial_culture", "polymorphic_load": "inline"}
def to_dict(self) -> dict:
output = super().to_dict()
output['controls'] = [item.to_sub_dict() for item in self.controls]
# logger.debug(f"{self.rsl_plate_num} technician: {output}")
return output
class Wastewater(BasicSubmission): class Wastewater(BasicSubmission):

View File

@@ -165,7 +165,7 @@ def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -
""" """
df = DataFrame.from_records(input) df = DataFrame.from_records(input)
safe = ['name', 'submitted_date', 'genus', 'target'] safe = ['name', 'submitted_date', 'genus', 'target']
logger.debug(df) # logger.debug(df)
for column in df.columns: for column in df.columns:
if "percent" in column: if "percent" in column:
count_col = [item for item in df.columns if "count" in item][0] count_col = [item for item in df.columns if "count" in item][0]

View File

@@ -35,6 +35,7 @@ import numpy
from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion
import logging import logging
import difflib import difflib
from getpass import getuser
from datetime import date from datetime import date
from frontend.visualizations.charts import create_charts from frontend.visualizations.charts import create_charts
@@ -65,7 +66,7 @@ class App(QMainWindow):
self._createMenuBar() self._createMenuBar()
self._createToolBar() self._createToolBar()
self._connectActions() self._connectActions()
self.controls_getter() self._controls_getter()
self.show() self.show()
@@ -109,10 +110,10 @@ class App(QMainWindow):
self.addReagentAction.triggered.connect(self.add_reagent) self.addReagentAction.triggered.connect(self.add_reagent)
self.generateReportAction.triggered.connect(self.generateReport) self.generateReportAction.triggered.connect(self.generateReport)
self.addKitAction.triggered.connect(self.add_kit) self.addKitAction.triggered.connect(self.add_kit)
self.table_widget.control_typer.currentIndexChanged.connect(self.controls_getter) self.table_widget.control_typer.currentIndexChanged.connect(self._controls_getter)
self.table_widget.mode_typer.currentIndexChanged.connect(self.controls_getter) self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter)
self.table_widget.datepicker.start_date.dateChanged.connect(self.controls_getter) self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter)
self.table_widget.datepicker.end_date.dateChanged.connect(self.controls_getter) self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter)
def importSubmission(self): def importSubmission(self):
@@ -268,6 +269,7 @@ class App(QMainWindow):
# logger.debug(info) # logger.debug(info)
# move samples into preliminary submission dict # move samples into preliminary submission dict
info['samples'] = self.samples info['samples'] = self.samples
info['uploaded_by'] = getuser()
# construct submission object # construct submission object
logger.debug(f"Here is the info_dict: {pprint.pformat(info)}") logger.debug(f"Here is the info_dict: {pprint.pformat(info)}")
base_submission, output = construct_submission_info(ctx=self.ctx, info_dict=info) base_submission, output = construct_submission_info(ctx=self.ctx, info_dict=info)
@@ -445,7 +447,7 @@ class App(QMainWindow):
def controls_getter(self): def _controls_getter(self):
""" """
Lookup controls from database and send to chartmaker Lookup controls from database and send to chartmaker
""" """
@@ -476,14 +478,14 @@ class App(QMainWindow):
with QSignalBlocker(self.table_widget.sub_typer) as blocker: with QSignalBlocker(self.table_widget.sub_typer) as blocker:
self.table_widget.sub_typer.addItems(sub_types) self.table_widget.sub_typer.addItems(sub_types)
self.table_widget.sub_typer.setEnabled(True) self.table_widget.sub_typer.setEnabled(True)
self.table_widget.sub_typer.currentTextChanged.connect(self.chart_maker) self.table_widget.sub_typer.currentTextChanged.connect(self._chart_maker)
else: else:
self.table_widget.sub_typer.clear() self.table_widget.sub_typer.clear()
self.table_widget.sub_typer.setEnabled(False) self.table_widget.sub_typer.setEnabled(False)
self.chart_maker() self._chart_maker()
def chart_maker(self): def _chart_maker(self):
""" """
Creates plotly charts for webview Creates plotly charts for webview
""" """

View File

@@ -5,14 +5,14 @@ from PyQt6.QtWidgets import (
QTextEdit, QSizePolicy, QWidget, QTextEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox, QGridLayout, QPushButton, QSpinBox,
QScrollBar, QScrollArea, QHBoxLayout, QScrollBar, QScrollArea, QHBoxLayout,
QMessageBox QMessageBox, QFileDialog, QToolBar
) )
from PyQt6.QtCore import Qt, QDate, QAbstractTableModel, QSize from PyQt6.QtCore import Qt, QDate, QAbstractTableModel, QSize
from PyQt6.QtGui import QFontMetrics from PyQt6.QtGui import QFontMetrics, QAction
from backend.db import get_all_reagenttype_names, submissions_to_df, lookup_submission_by_id, lookup_all_sample_types, create_kit_from_yaml from backend.db import get_all_reagenttype_names, submissions_to_df, lookup_submission_by_id, lookup_all_sample_types, create_kit_from_yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
@@ -212,23 +212,25 @@ class SubmissionDetails(QDialog):
def __init__(self, ctx:dict, id:int) -> None: def __init__(self, ctx:dict, id:int) -> None:
super().__init__() super().__init__()
self.ctx = ctx
self.setWindowTitle("Submission Details") self.setWindowTitle("Submission Details")
# create scrollable interior # create scrollable interior
interior = QScrollArea() interior = QScrollArea()
interior.setParent(self) interior.setParent(self)
# get submision from db # get submision from db
data = lookup_submission_by_id(ctx=ctx, id=id) data = lookup_submission_by_id(ctx=ctx, id=id)
base_dict = data.to_dict() self.base_dict = data.to_dict()
logger.debug(f"Base dict: {self.base_dict}")
# don't want id # don't want id
del base_dict['id'] del self.base_dict['id']
# convert sub objects to dicts # convert sub objects to dicts
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]
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
text = template.render(sub=base_dict) text = template.render(sub=self.base_dict)
# create text field # create text field
txt_editor = QTextEdit(self) txt_editor = QTextEdit(self)
txt_editor.setReadOnly(True) txt_editor.setReadOnly(True)
@@ -247,7 +249,37 @@ class SubmissionDetails(QDialog):
interior.setWidget(txt_editor) interior.setWidget(txt_editor)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setFixedSize(w, 900) self.setFixedSize(w, 900)
self.layout.addWidget(interior) btn = QPushButton("Export PDF")
btn.setParent(self)
btn.setFixedWidth(w)
btn.clicked.connect(self.export)
# def _create_actions(self):
# self.exportAction = QAction("Export", self)
def export(self):
template = env.get_template("submission_details.html")
html = template.render(sub=self.base_dict)
# logger.debug(f"Submission details: {self.base_dict}")
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
# logger.debug(f"report output name: {fname}")
# df.to_excel(fname, engine='openpyxl')
if fname.__str__() == ".":
logger.debug("Saving pdf was cancelled.")
return
try:
with open(fname, "w+b") as f:
pisa.CreatePDF(html, dest=f)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
msg = QMessageBox()
msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error")
msg.exec()

View File

@@ -94,7 +94,7 @@ def generic_figure_markers(fig:Figure, modes:list=[], ytitle:str|None=None) -> F
]) ])
) )
) )
logger.debug(f"Returning figure {fig}") # logger.debug(f"Returning figure {fig}")
assert type(fig) == Figure assert type(fig) == Figure
return fig return fig

View File

@@ -0,0 +1,32 @@
<!doctype html>
<html>
<head>
<title>Submission Details for {{ sub['Plate Number'] }}</title>
</head>
<body>
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>
{% for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' %}
{% if key=='Cost' %} <p>{{ key }}: {{ "${:,.2f}".format(value) }}</p> {% else %} <p>{{ key }}: {{ value }}</p> {% endif %}
{% endfor %}
<h3><u>Reagents:</u></h3>
{% for item in sub['reagents'] %}
<p>{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})</p>
{% endfor %}
<h3><u>Samples:</u></h3>
{% for item in sub['samples'] %}
<p>{{ item['well'] }}: {{ item['name'] }}</p>
{% endfor %}
{% if sub['controls'] %}
<h3><u>Attached Controls:</u></h3>
{% for item in sub['controls'] %}
<p><b>{{ item['name'] }}:</b> {{ item['type'] }} (Targets: {{ item['targets'] }})</p>
{% if item['kraken'] %}
<p>{{ item['name'] }} Top 5 Kraken Results</p>
{% for genera in item['kraken'] %}
<p>{{ genera['name'] }}: {{ genera['kraken_count'] }} ({{ genera['kraken_percent'] }})</p>
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
</body>
</html>

View File

@@ -1,6 +1,6 @@
{# template for constructing submission details #} {# template for constructing submission details #}
{% for key, value in sub.items() if key != 'reagents' and key != 'samples' %} {% for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' %}
{% if key=='Cost' %} {{ key }}: {{ "${:,.2f}".format(value) }} {% else %} {{ key }}: {{ value }} {% endif %} {% if key=='Cost' %} {{ key }}: {{ "${:,.2f}".format(value) }} {% else %} {{ key }}: {{ value }} {% endif %}
{% endfor %} {% endfor %}
Reagents: Reagents:
@@ -10,4 +10,14 @@ Reagents:
Samples: Samples:
{% for item in sub['samples'] %} {% for item in sub['samples'] %}
{{ item['well'] }}: {{ item['name'] }} {{ item['well'] }}: {{ item['name'] }}
{% endfor %} {% endfor %}
{% if sub['controls'] %}
Attached Controls:
{% for item in sub['controls'] %}
{{ item['name'] }}: {{ item['type'] }} (Targets: {{ item['targets'] }})
{% if item['kraken'] %}
{{ item['name'] }} Top 5 Kraken Results
{% for genera in item['kraken'] %}
{{ genera['name'] }}: {{ genera['kraken_count'] }} ({{ genera['kraken_percent'] }}){% endfor %}{% endif %}
{% endfor %}
{% endif %}