diff --git a/alembic/versions/785bb1140878_added_user_tracking.py b/alembic/versions/785bb1140878_added_user_tracking.py new file mode 100644 index 0000000..5709af8 --- /dev/null +++ b/alembic/versions/785bb1140878_added_user_tracking.py @@ -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 ### diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index e2a17e4..71cd9b3 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -1,4 +1,4 @@ # __init__.py # Version of the realpython-reader package -__version__ = "1.2.1" +__version__ = "1.2.2" diff --git a/src/submissions/backend/db/__init__.py b/src/submissions/backend/db/__init__.py index 0ad9c48..cfe0fc7 100644 --- a/src/submissions/backend/db/__init__.py +++ b/src/submissions/backend/db/__init__.py @@ -341,6 +341,12 @@ def submissions_to_df(ctx:dict, type:str|None=None) -> pd.DataFrame: # pass to lookup function subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)] 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 diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 61af626..5e73a54 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -1,6 +1,11 @@ from . import Base from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey from sqlalchemy.orm import relationship +import logging +from operator import itemgetter +import json + +logger = logging.getLogger(f"submissions.{__name__}") class ControlType(Base): """ @@ -35,3 +40,24 @@ class Control(Base): submission_id = Column(INTEGER, ForeignKey("_submissions.id")) #: parent submission id 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 + diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 7729711..445da54 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -2,6 +2,9 @@ from . import Base from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT from sqlalchemy.orm import relationship from datetime import datetime as dt +import logging + +logger = logging.getLogger(f"submissions.{__name__}") # 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"))) @@ -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 extraction_info = Column(JSON) #: unstructured output from the extraction table logger. run_cost = Column(FLOAT(2)) + uploaded_by = Column(String(32)) __mapper_args__ = { "polymorphic_identity": "basic_submission", @@ -77,6 +81,7 @@ class BasicSubmission(Base): "Technician": self.technician, "Cost": self.run_cost, } + logger.debug(f"{self.rsl_plate_num} technician: {output['Technician']}") return output @@ -107,6 +112,7 @@ class BasicSubmission(Base): # cost = self.extraction_kit.cost_per_run # except AttributeError: # cost = None + output = { "id": self.id, "Plate Number": self.rsl_plate_num, @@ -131,6 +137,13 @@ class BacterialCulture(BasicSubmission): 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")) __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): diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index a3bd59d..33373b8 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -165,7 +165,7 @@ def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) - """ df = DataFrame.from_records(input) safe = ['name', 'submitted_date', 'genus', 'target'] - logger.debug(df) + # logger.debug(df) for column in df.columns: if "percent" in column: count_col = [item for item in df.columns if "count" in item][0] diff --git a/src/submissions/frontend/__init__.py b/src/submissions/frontend/__init__.py index 9bbc141..4f19a43 100644 --- a/src/submissions/frontend/__init__.py +++ b/src/submissions/frontend/__init__.py @@ -35,6 +35,7 @@ import numpy from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion import logging import difflib +from getpass import getuser from datetime import date from frontend.visualizations.charts import create_charts @@ -65,7 +66,7 @@ class App(QMainWindow): self._createMenuBar() self._createToolBar() self._connectActions() - self.controls_getter() + self._controls_getter() self.show() @@ -109,10 +110,10 @@ class App(QMainWindow): self.addReagentAction.triggered.connect(self.add_reagent) self.generateReportAction.triggered.connect(self.generateReport) self.addKitAction.triggered.connect(self.add_kit) - self.table_widget.control_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.end_date.dateChanged.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.datepicker.start_date.dateChanged.connect(self._controls_getter) + self.table_widget.datepicker.end_date.dateChanged.connect(self._controls_getter) def importSubmission(self): @@ -268,6 +269,7 @@ class App(QMainWindow): # logger.debug(info) # move samples into preliminary submission dict info['samples'] = self.samples + info['uploaded_by'] = getuser() # construct submission object logger.debug(f"Here is the info_dict: {pprint.pformat(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 """ @@ -476,14 +478,14 @@ class App(QMainWindow): with QSignalBlocker(self.table_widget.sub_typer) as blocker: self.table_widget.sub_typer.addItems(sub_types) 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: self.table_widget.sub_typer.clear() 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 """ diff --git a/src/submissions/frontend/custom_widgets/__init__.py b/src/submissions/frontend/custom_widgets/__init__.py index bf5d429..5a059e0 100644 --- a/src/submissions/frontend/custom_widgets/__init__.py +++ b/src/submissions/frontend/custom_widgets/__init__.py @@ -5,14 +5,14 @@ from PyQt6.QtWidgets import ( QTextEdit, QSizePolicy, QWidget, QGridLayout, QPushButton, QSpinBox, QScrollBar, QScrollArea, QHBoxLayout, - QMessageBox + QMessageBox, QFileDialog, QToolBar ) 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 jinja2 import Environment, FileSystemLoader - +from xhtml2pdf import pisa import sys from pathlib import Path import logging @@ -212,23 +212,25 @@ class SubmissionDetails(QDialog): def __init__(self, ctx:dict, id:int) -> None: super().__init__() - + self.ctx = ctx self.setWindowTitle("Submission Details") + # create scrollable interior interior = QScrollArea() interior.setParent(self) # get submision from db 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 - del base_dict['id'] + del self.base_dict['id'] # convert sub objects to dicts - 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['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 - text = template.render(sub=base_dict) + text = template.render(sub=self.base_dict) # create text field txt_editor = QTextEdit(self) txt_editor.setReadOnly(True) @@ -247,7 +249,37 @@ class SubmissionDetails(QDialog): interior.setWidget(txt_editor) self.layout = QVBoxLayout() 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() diff --git a/src/submissions/frontend/visualizations/charts.py b/src/submissions/frontend/visualizations/charts.py index 48b79c4..c96f15e 100644 --- a/src/submissions/frontend/visualizations/charts.py +++ b/src/submissions/frontend/visualizations/charts.py @@ -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 return fig diff --git a/src/submissions/templates/submission_details.html b/src/submissions/templates/submission_details.html new file mode 100644 index 0000000..5f801c0 --- /dev/null +++ b/src/submissions/templates/submission_details.html @@ -0,0 +1,32 @@ + + +
+{{ key }}: {{ "${:,.2f}".format(value) }}
{% else %}{{ key }}: {{ value }}
{% endif %} + {% endfor %} +{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
+ {% endfor %} +{{ item['well'] }}: {{ item['name'] }}
+ {% endfor %} + {% if 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 %} + + \ No newline at end of file diff --git a/src/submissions/templates/submission_details.txt b/src/submissions/templates/submission_details.txt index 0d38f72..eceff58 100644 --- a/src/submissions/templates/submission_details.txt +++ b/src/submissions/templates/submission_details.txt @@ -1,6 +1,6 @@ {# 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 %} {% endfor %} Reagents: @@ -10,4 +10,14 @@ Reagents: Samples: {% for item in sub['samples'] %} {{ item['well'] }}: {{ item['name'] }} -{% endfor %} \ No newline at end of file +{% 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 %} \ No newline at end of file