diff --git a/CHANGELOG.md b/CHANGELOG.md index 9537f78..866c245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/TODO.md b/TODO.md index c075321..bdd2875 100644 --- a/TODO.md +++ b/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. \ No newline at end of file diff --git a/alembic.ini b/alembic.ini index 20ebdc4..37e6f8c 100644 --- a/alembic.ini +++ b/alembic.ini @@ -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] diff --git a/alembic/versions/a31943b2284c_added_commenting.py b/alembic/versions/a31943b2284c_added_commenting.py new file mode 100644 index 0000000..59ea0da --- /dev/null +++ b/alembic/versions/a31943b2284c_added_commenting.py @@ -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 ### diff --git a/alembic/versions/dc780c868efd_split_mutable_costs_into_96_and_24.py b/alembic/versions/dc780c868efd_split_mutable_costs_into_96_and_24.py deleted file mode 100644 index bf56000..0000000 --- a/alembic/versions/dc780c868efd_split_mutable_costs_into_96_and_24.py +++ /dev/null @@ -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 ### diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 3be1144..560bf9e 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -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 diff --git a/src/submissions/frontend/custom_widgets/sub_details.py b/src/submissions/frontend/custom_widgets/sub_details.py index 6ed37bf..7c0a988 100644 --- a/src/submissions/frontend/custom_widgets/sub_details.py +++ b/src/submissions/frontend/custom_widgets/sub_details.py @@ -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() + + \ No newline at end of file diff --git a/src/submissions/frontend/visualizations/__init__.py b/src/submissions/frontend/visualizations/__init__.py index ef76651..da0b5f0 100644 --- a/src/submissions/frontend/visualizations/__init__.py +++ b/src/submissions/frontend/visualizations/__init__.py @@ -1,4 +1,5 @@ ''' Contains all operations for creating charts, graphs and visual effects. ''' -from .control_charts import * \ No newline at end of file +from .control_charts import * +from .barcode import * \ No newline at end of file diff --git a/src/submissions/frontend/visualizations/barcode.py b/src/submissions/frontend/visualizations/barcode.py new file mode 100644 index 0000000..16ca96c --- /dev/null +++ b/src/submissions/frontend/visualizations/barcode.py @@ -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") \ No newline at end of file diff --git a/src/submissions/templates/submission_details.html b/src/submissions/templates/submission_details.html index 4a9c55d..89d46dd 100644 --- a/src/submissions/templates/submission_details.html +++ b/src/submissions/templates/submission_details.html @@ -3,7 +3,7 @@
{% for key, value in sub.items() if key not in excluded %} @@ -51,7 +51,7 @@ {% for entry in sub['ext_info'] %}
{% for key, value in entry.items() %}
- {% if loop.index == 1%}
+ {% if loop.index == 1 %}
{{ key|replace('_', ' ')|title() }}: {{ value }}
{% else %}
{% if "column" in key %}
@@ -71,7 +71,7 @@
{% for key, value in entry.items() if key != 'imported_by'%}
- {% if loop.index == 1%}
+ {% if loop.index == 1 %}
{{ key|replace('_', ' ')|title() }}: {{ value }}
{% else %}
{% if "column" in key %}
@@ -83,5 +83,15 @@
{% endfor %}
{% for entry in sub['comments'] %}
+ {% if loop.index == 1 %}
+ {{ entry['name'] }}:
{{ entry['text'] }}
- {{ entry['time'] }}
+ {% else %}
+ {{ entry['name'] }}:
{{ entry['text'] }}
- {{ entry['time'] }}
+ {% endif %}
+ {% endfor %}