Adding in commenting submissions and barcode creation.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
## 202305.02
|
||||
|
||||
- Added rudimentary barcode printing.
|
||||
- Added ability to comment on submissions.
|
||||
- Updated kit creation methods to keep pace with new cost calculations.
|
||||
|
||||
## 202305.01
|
||||
|
||||
2
TODO.md
2
TODO.md
@@ -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.
|
||||
@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
; 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\python\submissions\tests\test_assets\submissions_test.db
|
||||
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230427.db
|
||||
; msqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions_test.db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
33
alembic/versions/a31943b2284c_added_commenting.py
Normal file
33
alembic/versions/a31943b2284c_added_commenting.py
Normal 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 ###
|
||||
@@ -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 ###
|
||||
@@ -38,6 +38,7 @@ class BasicSubmission(Base):
|
||||
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.
|
||||
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.
|
||||
__mapper_args__ = {
|
||||
@@ -93,6 +94,11 @@ class BasicSubmission(Base):
|
||||
samples = [item.to_sub_dict() for item in self.samples]
|
||||
except:
|
||||
samples = None
|
||||
try:
|
||||
comments = self.comment
|
||||
except:
|
||||
logger.error(self.comment)
|
||||
comments = None
|
||||
output = {
|
||||
"id": self.id,
|
||||
"Plate Number": self.rsl_plate_num,
|
||||
@@ -106,9 +112,11 @@ class BasicSubmission(Base):
|
||||
"Cost": self.run_cost,
|
||||
"reagents": reagents,
|
||||
"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"Output dict: {output}")
|
||||
return output
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
'''
|
||||
Contains widgets specific to the submission summary and submission details.
|
||||
'''
|
||||
from datetime import datetime
|
||||
from PyQt6 import QtPrintSupport
|
||||
from PyQt6.QtWidgets import (
|
||||
QVBoxLayout, QDialog, QTableView,
|
||||
QTextEdit, QPushButton, QScrollArea,
|
||||
QMessageBox, QFileDialog, QMenu
|
||||
QMessageBox, QFileDialog, QMenu, QLabel,
|
||||
QDialogButtonBox, QToolBar, QMainWindow
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||
from PyQt6.QtGui import QFontMetrics, QAction, QCursor
|
||||
from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id
|
||||
from PyQt6.QtGui import QFontMetrics, QAction, QCursor, QPixmap, QPainter
|
||||
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 xhtml2pdf import pisa
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from .pop_ups import QuestionAsker
|
||||
from ..visualizations import make_plate_barcode
|
||||
from getpass import getuser
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -120,6 +125,22 @@ class SubmissionsSheet(QTableView):
|
||||
if dlg.exec():
|
||||
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):
|
||||
"""
|
||||
@@ -131,10 +152,16 @@ class SubmissionsSheet(QTableView):
|
||||
self.menu = QMenu(self)
|
||||
renameAction = QAction('Delete', self)
|
||||
detailsAction = QAction('Details', self)
|
||||
barcodeAction = QAction("Print Barcode", self)
|
||||
commentAction = QAction("Add Comment", self)
|
||||
renameAction.triggered.connect(lambda: self.delete_item(event))
|
||||
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(renameAction)
|
||||
self.menu.addAction(barcodeAction)
|
||||
self.menu.addAction(commentAction)
|
||||
# add other required actions
|
||||
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.setWindowTitle("Permission Error")
|
||||
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()
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
Contains all operations for creating charts, graphs and visual effects.
|
||||
'''
|
||||
from .control_charts import *
|
||||
from .barcode import *
|
||||
8
src/submissions/frontend/visualizations/barcode.py
Normal file
8
src/submissions/frontend/visualizations/barcode.py
Normal 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")
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||
</head>
|
||||
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info'] %}
|
||||
{% set excluded = ['reagents', 'samples', 'controls', 'ext_info', 'pcr_info', 'comments'] %}
|
||||
<body>
|
||||
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2>
|
||||
<p>{% for key, value in sub.items() if key not in excluded %}
|
||||
@@ -51,7 +51,7 @@
|
||||
{% for entry in sub['ext_info'] %}
|
||||
<h3><u>Extraction Status:</u></h3>
|
||||
<p>{% for key, value in entry.items() %}
|
||||
{% if loop.index == 1%}
|
||||
{% if loop.index == 1 %}
|
||||
<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br>
|
||||
{% else %}
|
||||
{% if "column" in key %}
|
||||
@@ -71,7 +71,7 @@
|
||||
<h3><u>qPCR Status:</u></h3>
|
||||
{% endif %}
|
||||
<p>{% for key, value in entry.items() if key != 'imported_by'%}
|
||||
{% if loop.index == 1%}
|
||||
{% if loop.index == 1 %}
|
||||
<b>{{ key|replace('_', ' ')|title() }}:</b> {{ value }}<br>
|
||||
{% else %}
|
||||
{% if "column" in key %}
|
||||
@@ -83,5 +83,15 @@
|
||||
{% endfor %}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if sub['comments'] %}
|
||||
<h3><u>Comments:</u></h3>
|
||||
<p>{% for entry in sub['comments'] %}
|
||||
{% if loop.index == 1 %}
|
||||
<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
|
||||
{% else %}
|
||||
<b>{{ entry['name'] }}:</b><br> {{ entry['text'] }}<br>- {{ entry['time'] }}<br>
|
||||
{% endif %}
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
{# 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 not in excluded %}
|
||||
{% 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' %}
|
||||
{{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}{% endfor %}
|
||||
{% endif %}
|
||||
{% if sub['comments'] %}
|
||||
Comments:
|
||||
{% for item in sub['comments'] %}
|
||||
{{ item['name'] }}:
|
||||
{{ item['text'] }}
|
||||
- {{ item['time'] }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -195,7 +195,7 @@ class RSLNamer(object):
|
||||
return
|
||||
logger.debug(f"Attempting match of {in_str}")
|
||||
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})
|
||||
""", flags = re.IGNORECASE | re.VERBOSE)
|
||||
m = regex.search(in_str)
|
||||
|
||||
Reference in New Issue
Block a user