Adding in commenting submissions and barcode creation.

This commit is contained in:
Landon Wark
2023-05-12 14:26:18 -05:00
parent 8a95e3a4c5
commit 903f403672
12 changed files with 231 additions and 64 deletions

View File

@@ -1,5 +1,7 @@
## 202305.02 ## 202305.02
- Added rudimentary barcode printing.
- Added ability to comment on submissions.
- Updated kit creation methods to keep pace with new cost calculations. - Updated kit creation methods to keep pace with new cost calculations.
## 202305.01 ## 202305.01

View File

@@ -1 +1,3 @@
- [ ] Create a method for commenting submissions.
- [ ] Create barcode generator, because of reasons that may or may not exist.
- [x] Move bulk of functions from frontend.__init__ to frontend.functions as __init__ is getting bloated. - [x] Move bulk of functions from frontend.__init__ to frontend.functions as __init__ is getting bloated.

View File

@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230427.db ; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230427.db
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions_test.db ; msqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions_test.db
[post_write_hooks] [post_write_hooks]

View File

@@ -0,0 +1,33 @@
"""added commenting
Revision ID: a31943b2284c
Revises: 83b06f3f4869
Create Date: 2023-05-10 11:34:30.339915
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a31943b2284c'
down_revision = '83b06f3f4869'
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('comment', sa.JSON(), 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('comment')
# ### end Alembic commands ###

View File

@@ -1,51 +0,0 @@
"""split mutable costs into 96 and 24
Revision ID: dc780c868efd
Revises: 83b06f3f4869
Create Date: 2023-05-01 14:05:47.762441
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = 'dc780c868efd'
down_revision = '83b06f3f4869'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_alembic_tmp__kits')
with op.batch_alter_table('_kits', schema=None) as batch_op:
batch_op.add_column(sa.Column('mutable_cost_96', sa.FLOAT(precision=2), nullable=True))
batch_op.add_column(sa.Column('mutable_cost_24', sa.FLOAT(precision=2), nullable=True))
batch_op.drop_column('mutable_cost')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_kits', schema=None) as batch_op:
batch_op.add_column(sa.Column('mutable_cost', sa.FLOAT(), nullable=True))
batch_op.drop_column('mutable_cost_24')
batch_op.drop_column('mutable_cost_96')
op.create_table('_alembic_tmp__kits',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.VARCHAR(length=64), nullable=True),
sa.Column('used_for', sqlite.JSON(), nullable=True),
sa.Column('cost_per_run', sa.FLOAT(), nullable=True),
sa.Column('reagent_types_id', sa.INTEGER(), nullable=True),
sa.Column('constant_cost', sa.FLOAT(), nullable=True),
sa.Column('mutable_cost_96', sa.FLOAT(), nullable=True),
sa.Column('mutable_cost_24', sa.FLOAT(), nullable=True),
sa.ForeignKeyConstraint(['reagent_types_id'], ['_reagent_types.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
# ### end Alembic commands ###

View File

@@ -38,6 +38,7 @@ class BasicSubmission(Base):
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)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation. run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation.
uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database. uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database.
comment = Column(JSON)
# Allows for subclassing into ex. BacterialCulture, Wastewater, etc. # Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
__mapper_args__ = { __mapper_args__ = {
@@ -93,6 +94,11 @@ class BasicSubmission(Base):
samples = [item.to_sub_dict() for item in self.samples] samples = [item.to_sub_dict() for item in self.samples]
except: except:
samples = None samples = None
try:
comments = self.comment
except:
logger.error(self.comment)
comments = None
output = { output = {
"id": self.id, "id": self.id,
"Plate Number": self.rsl_plate_num, "Plate Number": self.rsl_plate_num,
@@ -106,9 +112,11 @@ class BasicSubmission(Base):
"Cost": self.run_cost, "Cost": self.run_cost,
"reagents": reagents, "reagents": reagents,
"samples": samples, "samples": samples,
"ext_info": ext_info "ext_info": ext_info,
"comments": comments
} }
# logger.debug(f"{self.rsl_plate_num} extraction: {output['Extraction Status']}") # logger.debug(f"{self.rsl_plate_num} extraction: {output['Extraction Status']}")
# logger.debug(f"Output dict: {output}")
return output return output

View File

@@ -1,20 +1,25 @@
''' '''
Contains widgets specific to the submission summary and submission details. Contains widgets specific to the submission summary and submission details.
''' '''
from datetime import datetime
from PyQt6 import QtPrintSupport
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView, QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea, QTextEdit, QPushButton, QScrollArea,
QMessageBox, QFileDialog, QMenu QMessageBox, QFileDialog, QMenu, QLabel,
QDialogButtonBox, QToolBar, QMainWindow
) )
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QFontMetrics, QAction, QCursor from PyQt6.QtGui import QFontMetrics, QAction, QCursor, QPixmap, QPainter
from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id, lookup_submission_by_rsl_num
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 QuestionAsker from .pop_ups import QuestionAsker
from ..visualizations import make_plate_barcode
from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -120,6 +125,22 @@ class SubmissionsSheet(QTableView):
if dlg.exec(): if dlg.exec():
pass pass
def create_barcode(self) -> None:
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),1).data()
logger.debug(f"Selected value: {value}")
dlg = BarcodeWindow(value)
if dlg.exec():
dlg.print_barcode()
def add_comment(self) -> None:
index = (self.selectionModel().currentIndex())
value = index.sibling(index.row(),1).data()
logger.debug(f"Selected value: {value}")
dlg = SubmissionComment(ctx=self.ctx, rsl=value)
if dlg.exec():
dlg.add_comment()
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
""" """
@@ -131,10 +152,16 @@ class SubmissionsSheet(QTableView):
self.menu = QMenu(self) self.menu = QMenu(self)
renameAction = QAction('Delete', self) renameAction = QAction('Delete', self)
detailsAction = QAction('Details', self) detailsAction = QAction('Details', self)
barcodeAction = QAction("Print Barcode", self)
commentAction = QAction("Add Comment", self)
renameAction.triggered.connect(lambda: self.delete_item(event)) renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details()) detailsAction.triggered.connect(lambda: self.show_details())
barcodeAction.triggered.connect(lambda: self.create_barcode())
commentAction.triggered.connect(lambda: self.add_comment())
self.menu.addAction(detailsAction) self.menu.addAction(detailsAction)
self.menu.addAction(renameAction) self.menu.addAction(renameAction)
self.menu.addAction(barcodeAction)
self.menu.addAction(commentAction)
# add other required actions # add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
@@ -227,3 +254,122 @@ class SubmissionDetails(QDialog):
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.") msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error") msg.setWindowTitle("Permission Error")
msg.exec() msg.exec()
class BarcodeWindow(QDialog):
def __init__(self, rsl_num:str):
super().__init__()
# set the title
self.setWindowTitle("Image")
self.layout = QVBoxLayout()
# setting the geometry of window
self.setGeometry(0, 0, 400, 300)
# creating label
self.label = QLabel()
self.img = make_plate_barcode(rsl_num)
# logger.debug(dir(img), img.contents[0])
# fp = BytesIO().read()
# img.save(formats=['png'], fnRoot=fp)
# pixmap = QPixmap("C:\\Users\\lwark\\Documents\\python\\submissions\\src\\Drawing000.png")
self.pixmap = QPixmap()
# self.pixmap.loadFromData(self.img.asString("bmp"))
self.pixmap.loadFromData(self.img)
# adding image to label
self.label.setPixmap(self.pixmap)
# Optional, resize label to image size
# self.label.resize(self.pixmap.width(), self.pixmap.height())
# self.label.resize(200, 200)
# show all the widgets]
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.label)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
self._createActions()
self._createToolBar()
self._connectActions()
def _createToolBar(self):
"""
adds items to menu bar
"""
toolbar = QToolBar("My main toolbar")
# self.addToolBar(toolbar)
# self.layout.setToolBar(toolbar)
toolbar.addAction(self.printAction)
def _createActions(self):
"""
creates actions
"""
self.printAction = QAction("&Print", self)
def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
self.printAction.triggered.connect(self.print_barcode)
def print_barcode(self):
printer = QtPrintSupport.QPrinter()
dialog = QtPrintSupport.QPrintDialog(printer)
if dialog.exec():
self.handle_paint_request(printer, self.pixmap.toImage())
def handle_paint_request(self, printer:QtPrintSupport.QPrinter, im):
logger.debug(f"Hello from print handler.")
painter = QPainter(printer)
image = QPixmap.fromImage(im)
painter.drawPixmap(120, -20, image)
painter.end()
class SubmissionComment(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, ctx:dict, rsl:str) -> None:
super().__init__()
self.ctx = ctx
self.rsl = rsl
self.setWindowTitle(f"{self.rsl} Submission Comment")
# create text field
self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False)
self.txt_editor.setText("Add Comment")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
self.setFixedSize(400, 300)
self.layout.addWidget(self.txt_editor)
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def add_comment(self):
commenter = getuser()
comment = self.txt_editor.toPlainText()
dt = datetime.strftime(datetime.now(), "%Y-%m-d %H:%M:%S")
full_comment = {"name":commenter, "time": dt, "text": comment}
logger.debug(f"Full comment: {full_comment}")
sub = lookup_submission_by_rsl_num(ctx = self.ctx, rsl_num=self.rsl)
try:
sub.comment.append(full_comment)
except AttributeError:
sub.comment = [full_comment]
logger.debug(sub.__dict__)
self.ctx['database_session'].add(sub)
self.ctx['database_session'].commit()

View File

@@ -2,3 +2,4 @@
Contains all operations for creating charts, graphs and visual effects. Contains all operations for creating charts, graphs and visual effects.
''' '''
from .control_charts import * from .control_charts import *
from .barcode import *

View File

@@ -0,0 +1,8 @@
from reportlab.graphics.barcode import createBarcodeDrawing, createBarcodeImageInMemory
from reportlab.graphics.shapes import Drawing
from reportlab.lib.units import mm
def make_plate_barcode(text:str) -> Drawing:
# return createBarcodeDrawing('Code128', value=text, width=200, height=50, humanReadable=True)
return createBarcodeImageInMemory('Code128', value=text, width=100*mm, height=25*mm, humanReadable=True, format="png")

View File

@@ -3,7 +3,7 @@
<head> <head>
<title>Submission Details for {{ sub['Plate Number'] }}</title> <title>Submission Details for {{ sub['Plate Number'] }}</title>
</head> </head>
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info'] %} {% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments'] %}
<body> <body>
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2> <h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>
<p>{% for key, value in sub.items() if key not in excluded %} <p>{% for key, value in sub.items() if key not in excluded %}
@@ -51,7 +51,7 @@
{% for entry in sub['ext_info'] %} {% for entry in sub['ext_info'] %}
<h3><u>Extraction Status:</u></h3> <h3><u>Extraction Status:</u></h3>
<p>{% for key, value in entry.items() %} <p>{% for key, value in entry.items() %}
{% if loop.index == 1%} {% if loop.index == 1 %}
&nbsp;&nbsp;&nbsp;<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br> &nbsp;&nbsp;&nbsp;<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br>
{% else %} {% else %}
{% if "column" in key %} {% if "column" in key %}
@@ -71,7 +71,7 @@
<h3><u>qPCR Status:</u></h3> <h3><u>qPCR Status:</u></h3>
{% endif %} {% endif %}
<p>{% for key, value in entry.items() if key != 'imported_by'%} <p>{% for key, value in entry.items() if key != 'imported_by'%}
{% if loop.index == 1%} {% if loop.index == 1 %}
&nbsp;&nbsp;&nbsp;<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br> &nbsp;&nbsp;&nbsp;<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br>
{% else %} {% else %}
{% if "column" in key %} {% if "column" in key %}
@@ -83,5 +83,15 @@
{% endfor %}</p> {% endfor %}</p>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if sub['comments'] %}
<h3><u>Comments:</u></h3>
<p>{% for entry in sub['comments'] %}
{% if loop.index == 1 %}
&nbsp;&nbsp;&nbsp;<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
{% else %}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
{% endif %}
{% endfor %}</p>
{% endif %}
</body> </body>
</html> </html>

View File

@@ -1,5 +1,5 @@
{# template for constructing submission details #} {# template for constructing submission details #}
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info'] %} {% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments'] %}
{# for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' and key != 'ext_info' #} {# for key, value in sub.items() if key != 'reagents' and key != 'samples' and key != 'controls' and key != 'ext_info' #}
{% for key, value in sub.items() if key not in excluded %} {% for key, value in sub.items() if key not in excluded %}
{% if key=='Cost' %} {{ key }}: {{ "${:,.2f}".format(value) }} {% else %} {{ key }}: {{ value }} {% endif %} {% if key=='Cost' %} {{ key }}: {{ "${:,.2f}".format(value) }} {% else %} {{ key }}: {{ value }} {% endif %}
@@ -30,3 +30,11 @@ qPCR Status{% endif %}
{% for key, value in entry.items() if key != 'imported_by' %} {% for key, value in entry.items() if key != 'imported_by' %}
{{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}{% endfor %} {{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}{% endfor %}
{% endif %} {% endif %}
{% if sub['comments'] %}
Comments:
{% for item in sub['comments'] %}
{{ item['name'] }}:
{{ item['text'] }}
- {{ item['time'] }}
{% endfor %}
{% endif %}

View File

@@ -195,7 +195,7 @@ class RSLNamer(object):
return return
logger.debug(f"Attempting match of {in_str}") logger.debug(f"Attempting match of {in_str}")
regex = re.compile(r""" regex = re.compile(r"""
(?P<wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(?:_|-)\d(?!\d))?)| (?P<wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(?:_|-)R?\d(?!\d))?)|
(?P<bacterial_culture>RSL-?\d{2}-?\d{4}) (?P<bacterial_culture>RSL-?\d{2}-?\d{4})
""", flags = re.IGNORECASE | re.VERBOSE) """, flags = re.IGNORECASE | re.VERBOSE)
m = regex.search(in_str) m = regex.search(in_str)