Improved navigation and clarity in details view.

This commit is contained in:
lwark
2024-09-13 15:23:11 -05:00
parent 744394f236
commit c7d83401e7
15 changed files with 270 additions and 177 deletions

View File

@@ -8,12 +8,13 @@ import json
from pprint import pprint, pformat
import yaml
from jinja2 import TemplateNotFound
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date
import logging, re
from tools import check_authorization, setup_lookup, Report, Result
from tools import check_authorization, setup_lookup, Report, Result, jinja_template_loading
from typing import List, Literal, Generator, Any
from pandas import ExcelFile
from pathlib import Path
@@ -421,12 +422,13 @@ class Reagent(BaseClass):
else:
return f"<Reagent({self.role.name}-{self.lot})>"
def to_sub_dict(self, extraction_kit: KitType = None) -> dict:
def to_sub_dict(self, extraction_kit: KitType = None, full_data:bool=False) -> dict:
"""
dictionary containing values necessary for gui
Args:
extraction_kit (KitType, optional): KitType to use to get reagent type. Defaults to None.
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
Returns:
dict: representation of the reagent's attributes
@@ -456,13 +458,17 @@ class Reagent(BaseClass):
place_holder = "NA"
else:
place_holder = place_holder.strftime("%Y-%m-%d")
return dict(
output = dict(
name=self.name,
role=rtype,
lot=self.lot,
expiry=place_holder,
missing=False
)
if full_data:
output['submissions'] = [sub.rsl_plate_num for sub in self.submissions]
output['excluded'] = ['missing', 'submissions', 'excluded']
return output
def update_last_used(self, kit: KitType) -> Report:
"""

View File

@@ -168,7 +168,8 @@ class BasicSubmission(BaseClass):
'extraction_info', 'comment', 'barcode',
'platemap', 'export_map', 'equipment', 'tips'],
# NOTE: Fields not placed in ui form
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer', 'submission_object', "tips"] + recover,
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
'submission_object', "tips", 'contact_phone'] + recover,
# NOTE: Fields not placed in ui form to be moved to pydantic
form_recover=recover
)
@@ -229,7 +230,8 @@ class BasicSubmission(BaseClass):
return SubmissionType.query(cls.__mapper_args__['polymorphic_identity'])
@classmethod
def construct_info_map(cls, submission_type:SubmissionType|None=None, mode: Literal["read", "write"]="read") -> dict:
def construct_info_map(cls, submission_type: SubmissionType | None = None,
mode: Literal["read", "write"] = "read") -> dict:
"""
Method to call submission type's construct info map.
@@ -242,7 +244,7 @@ class BasicSubmission(BaseClass):
return cls.get_submission_type(submission_type).construct_info_map(mode=mode)
@classmethod
def construct_sample_map(cls, submission_type:SubmissionType|None=None) -> dict:
def construct_sample_map(cls, submission_type: SubmissionType | None = None) -> dict:
"""
Method to call submission type's construct_sample_map
@@ -609,7 +611,7 @@ class BasicSubmission(BaseClass):
new_dict = {}
for key, value in dicto.items():
# logger.debug(f"Checking {key}")
missing = value is None or value in ['', 'None']
missing = value in ['', 'None', None]
match key:
case "reagents":
new_dict[key] = [PydReagent(**reagent) for reagent in value]
@@ -628,7 +630,7 @@ class BasicSubmission(BaseClass):
case "id":
pass
case _:
# logger.debug(f"Setting dict {key} to {value}")
logger.debug(f"Setting dict {key} to {value}")
new_dict[key.lower().replace(" ", "_")] = dict(value=value, missing=missing)
# logger.debug(f"{key} complete after {time()-start}")
new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
@@ -648,7 +650,7 @@ class BasicSubmission(BaseClass):
return super().save()
@classmethod
def get_regex(cls, submission_type:SubmissionType|str|None=None):
def get_regex(cls, submission_type: SubmissionType | str | None = None):
# logger.debug(f"Attempting to get regex for {cls.__mapper_args__['polymorphic_identity']}")
logger.debug(f"Attempting to get regex for {submission_type}")
try:
@@ -707,7 +709,8 @@ class BasicSubmission(BaseClass):
# item.__mapper_args__['polymorphic_identity'] == polymorphic_identity][0]
model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_
except Exception as e:
logger.error(f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicSubmission")
logger.error(
f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicSubmission")
case _:
pass
if attrs is None or len(attrs) == 0:
@@ -1199,6 +1202,8 @@ class BasicSubmission(BaseClass):
dlg = SubmissionComment(parent=obj, submission=self)
if dlg.exec():
comment = dlg.parse_form()
if comment in ["", None]:
return
self.set_attribute(key='comment', value=comment)
# logger.debug(self.comment)
self.save(original=False)
@@ -1368,49 +1373,6 @@ class BacterialCulture(BasicSubmission):
return input_dict
# class ViralCulture(BasicSubmission):
#
# id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
# __mapper_args__ = dict(polymorphic_identity="Viral Culture",
# polymorphic_load="inline",
# inherit_condition=(id == BasicSubmission.id))
#
# # @classmethod
# # def get_regex(cls) -> str:
# # """
# # Retrieves string for regex construction.
# #
# # Returns:
# # str: string for regex construction
# # """
# # return "(?P<Viral_Culture>RSL(?:-|_)?VE(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\sA-QS-Z]|$)?R?\d?)?)"
#
# @classmethod
# def custom_sample_autofill_row(cls, sample, worksheet: Worksheet) -> int:
# """
# Extends parent
# """
# # logger.debug(f"Checking {sample.well}")
# # logger.debug(f"here's the worksheet: {worksheet}")
# row = super().custom_sample_autofill_row(sample, worksheet)
# df = pd.DataFrame(list(worksheet.values))
# # logger.debug(f"Here's the dataframe: {df}")
# idx = df[df[0] == sample.well]
# if idx.empty:
# new = f"{sample.well[0]}{sample.well[1:].zfill(2)}"
# # logger.debug(f"Checking: {new}")
# idx = df[df[0] == new]
# # logger.debug(f"Here is the row: {idx}")
# row = idx.index.to_list()[0]
# return row + 1
#
# @classmethod
# def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None, custom_fields: dict = {}) -> dict:
# input_dict = super().custom_info_parser(input_dict=input_dict, xl=xl, custom_fields=custom_fields)
# logger.debug(f"\n\nInfo dictionary:\n\n{pformat(input_dict)}\n\n")
# return input_dict
class Wastewater(BasicSubmission):
"""
derivative submission type from BasicSubmission
@@ -2239,6 +2201,9 @@ class BasicSample(BaseClass):
"""
gui friendly dictionary, extends parent method.
Args:
full_data (bool): Whether to use full object or truncated. Defaults to False
Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""

View File

@@ -137,6 +137,7 @@ class ReportDatePicker(QDialog):
"""
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
class LogParser(QDialog):
def __init__(self, parent):

View File

@@ -1,14 +1,16 @@
"""
Webview to show submission and sample details.
"""
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit)
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit, QGridLayout)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample
from tools import is_power_user, html_to_pdf
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from tools import is_power_user, html_to_pdf, jinja_template_loading
from .functions import select_save_file
from pathlib import Path
import logging
@@ -25,8 +27,9 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, parent, sub:BasicSubmission|BasicSample) -> None:
"""
def __init__(self, parent, sub: BasicSubmission | BasicSample | Reagent) -> None:
super().__init__(parent)
try:
@@ -35,15 +38,20 @@ class SubmissionDetails(QDialog):
self.app = None
self.webview = QWebEngineView(parent=self)
self.webview.setMinimumSize(900, 500)
self.webview.setMaximumSize(900, 500)
self.layout = QVBoxLayout()
self.setFixedSize(900, 500)
self.webview.setMaximumSize(900, 700)
self.webview.loadFinished.connect(self.activate_export)
self.layout = QGridLayout()
# self.setFixedSize(900, 500)
# NOTE: button to export a pdf version
btn = QPushButton("Export DOCX")
btn.setFixedWidth(875)
btn.clicked.connect(self.export)
self.layout.addWidget(btn)
self.layout.addWidget(self.webview)
self.btn = QPushButton("Export DOCX")
self.btn.setFixedWidth(775)
self.btn.clicked.connect(self.export)
self.back = QPushButton("Back")
self.back.setFixedWidth(100)
self.back.clicked.connect(self.back_function)
self.layout.addWidget(self.back, 0, 0, 1, 1)
self.layout.addWidget(self.btn, 0, 1, 1, 9)
self.layout.addWidget(self.webview, 1, 0, 10, 10)
self.setLayout(self.layout)
# NOTE: setup channel
self.channel = QWebChannel()
@@ -54,10 +62,26 @@ class SubmissionDetails(QDialog):
self.rsl_plate_num = sub.rsl_plate_num
case BasicSample():
self.sample_details(sample=sub)
case Reagent():
self.reagent_details(reagent=sub)
self.webview.page().setWebChannel(self.channel)
def back_function(self):
self.webview.back()
# @pyqtSlot(bool)
def activate_export(self):
title = self.webview.title()
self.setWindowTitle(title)
if "Submission" in title:
self.btn.setEnabled(True)
self.export_plate = title.split(" ")[-1]
logger.debug(f"Updating export plate to: {self.export_plate}")
else:
self.btn.setEnabled(False)
@pyqtSlot(str)
def sample_details(self, sample:str|BasicSample):
def sample_details(self, sample: str | BasicSample):
"""
Changes details view to summary of Sample
@@ -68,21 +92,49 @@ class SubmissionDetails(QDialog):
sample = BasicSample.query(submitter_id=sample)
base_dict = sample.to_sub_dict(full_data=True)
base_dict, template = sample.get_details_template(base_dict=base_dict)
html = template.render(sample=base_dict)
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
html = template.render(sample=base_dict, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"Sample Details - {sample.submitter_id}")
# self.btn.setEnabled(False)
@pyqtSlot(str, str)
def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
if isinstance(reagent, str):
reagent = Reagent.query(lot_number=reagent)
if isinstance(kit, str):
kit = KitType.query(name=kit)
base_dict = reagent.to_sub_dict(extraction_kit=kit, full_data=True)
env = jinja_template_loading()
temp_name = "reagent_details.html"
# logger.debug(f"Returning template: {temp_name}")
try:
template = env.get_template(temp_name)
except TemplateNotFound as e:
logger.error(f"Couldn't find template due to {e}")
return
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
html = template.render(reagent=base_dict, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"Reagent Details - {reagent.name} - {reagent.lot}")
# self.btn.setEnabled(False)
@pyqtSlot(str)
def submission_details(self, submission:str|BasicSubmission):
def submission_details(self, submission: str | BasicSubmission):
"""
Sets details view to summary of Submission.
Args:
submission (str | BasicSubmission): Submission of interest.
"""
"""
# logger.debug(f"Details for: {submission}")
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
self.rsl_plate_num = submission.rsl_plate_num
self.base_dict = submission.to_dict(full_data=True)
# logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k == 'reagents'})}")
# NOTE: don't want id
@@ -97,10 +149,10 @@ class SubmissionDetails(QDialog):
# logger.debug(f"Submission_details: {pformat(self.base_dict)}")
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css)
self.webview.setHtml(self.html)
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
# self.btn.setEnabled(True)
@pyqtSlot(str)
def sign_off(self, submission:str|BasicSubmission):
def sign_off(self, submission: str | BasicSubmission):
# logger.debug(f"Signing off on {submission} - ({getuser()})")
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
@@ -112,19 +164,23 @@ class SubmissionDetails(QDialog):
"""
Renders submission to html, then creates and saves .pdf file to user selected file.
"""
writer = DocxWriter(base_dict=self.base_dict)
fname = select_save_file(obj=self, default_name=self.base_dict['plate_number'], extension="docx")
export_plate = BasicSubmission.query(rsl_plate_num=self.export_plate)
base_dict = export_plate.to_dict(full_data=True)
writer = DocxWriter(base_dict=base_dict)
fname = select_save_file(obj=self, default_name=base_dict['plate_number'], extension="docx")
writer.save(fname)
try:
html_to_pdf(html=self.html, output_file=fname)
except PermissionError as e:
logger.error(f"Error saving pdf: {e}")
class SubmissionComment(QDialog):
"""
a window for adding comment text to a submission
"""
def __init__(self, parent, submission:BasicSubmission) -> None:
"""
def __init__(self, parent, submission: BasicSubmission) -> None:
super().__init__(parent)
try:
@@ -137,7 +193,8 @@ class SubmissionComment(QDialog):
# NOTE: create text field
self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False)
self.txt_editor.setText("Add Comment")
self.txt_editor.setPlaceholderText("Write your comment here.")
self.txt_editor.setStyleSheet("background-color: rgb(255, 255, 255);")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
@@ -147,14 +204,16 @@ class SubmissionComment(QDialog):
self.layout.addWidget(self.txt_editor)
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout)
def parse_form(self) -> List[dict]:
"""
Adds comment to submission object.
"""
"""
commenter = getuser()
comment = self.txt_editor.toPlainText()
if comment in ["", None]:
return None
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
full_comment = {"name":commenter, "time": dt, "text": comment}
full_comment = {"name": commenter, "time": dt, "text": comment}
# logger.debug(f"Full comment: {full_comment}")
return full_comment

View File

@@ -257,7 +257,7 @@ class SubmissionsSheet(QTableView):
dlg = ReportDatePicker()
if dlg.exec():
info = dlg.parse_form()
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx")
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx", extension="xlsx")
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
rp.write_report(filename=fname, obj=self)
return report

View File

@@ -191,7 +191,9 @@ class SubmissionFormWidget(QWidget):
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
self.layout = QVBoxLayout()
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
logger.debug(f"Creating widget: {k}")
if k in self.ignore:
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
continue
try:
# logger.debug(f"Key: {k}, Disable: {disable}")
@@ -201,9 +203,12 @@ class SubmissionFormWidget(QWidget):
check = False
try:
value = self.pyd.__getattribute__(k)
except AttributeError:
logger.error(f"Couldn't get attribute from pyd: {k}")
value = dict(value=None, missing=True)
except AttributeError as e:
logger.error(f"Couldn't get attribute from pyd: {k} due to {e}")
try:
value = self.pyd.model_extra[k]
except KeyError:
value = dict(value=None, missing=True)
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'],
sub_obj=st, disable=check)
if add_widget is not None:
@@ -259,7 +264,7 @@ class SubmissionFormWidget(QWidget):
Tuple[QMainWindow, dict]: Updated application and result
"""
extraction_kit = args[0]
caller = inspect.stack()[1].function.__repr__().replace("'", "")
# caller = inspect.stack()[1].function.__repr__().replace("'", "")
# logger.debug(f"Self.reagents: {self.reagents}")
# logger.debug(f"\n\n{pformat(caller)}\n\n")
# logger.debug(f"SubmissionType: {self.submission_type}")

View File

@@ -1,65 +1,37 @@
<!doctype html>
{% extends "details.html" %}
<html>
<head>
{% block head %}
<style>
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
/* Position the tooltip text - see examples below! */
position: absolute;
z-index: 1;
bottom: 100%;
left: 50%;
margin-left: -60px;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
font-size: large;
}
</style>
<title>Sample Details for {{ sample['Submitter ID'] }}</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{{ super() }}
<title>Sample Details for {{ sample['submitter_id'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Sample Details for {{ sample['Submitter ID'] }}</u></h2>
<h2><u>Sample Details for {{ sample['submitter_id'] }}</u></h2>
{{ super() }}
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
{% endfor %}</p>
{% if sample['submissions'] %}<h2>Submissions:</h2>
{% for submission in sample['submissions'] %}
<p id="{{ submission['plate_name'] }}"><b>{{ submission['plate_name'] }}:</b> {{ submission['well'] }}</p>
<p><b><a class="data-link" id="{{ submission['plate_name'] }}">{{ submission['plate_name'] }}:</a></b> {{ submission['well'] }}</p>
{% endfor %}
{% endif %}
{% endblock %}
</body>
<script>
var backend;
new QWebChannel(qt.webChannelTransport, function (channel) {
backend = channel.objects.backend;
});
{% block script %}
{{ super() }}
{% for submission in sample['submissions'] %}
document.getElementById("{{ submission['plate_name'] }}").addEventListener("dblclick", function(){
document.getElementById("{{ submission['plate_name'] }}").addEventListener("click", function(){
backend.submission_details("{{ submission['plate_name'] }}");
});
{% endfor %}
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
{% endblock %}
</script>
</html>

View File

@@ -1,26 +1,25 @@
<!doctype html>
{% extends "details.html" %}
<html>
<head>
{% block head %}
{% if css %}
<style>
{{ css }}
</style>
{% endif %}
<title>Submission Details for {{ sub['Plate Number'] }}</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{{ super() }}
<title>Submission Details for {{ sub['plate_number'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Submission Details for {{ sub['plate_number'] }}</u></h2>&nbsp;&nbsp;&nbsp;{% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
{{ super() }}
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
{% endfor %}</p>
<h3><u>Reagents:</u></h3>
<p>{% for item in sub['reagents'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}</b>: <a class="data-link" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
{% endfor %}</p>
{% if sub['equipment'] %}
<h3><u>Equipment:</u></h3>
<p>{% for item in sub['equipment'] %}
@@ -36,7 +35,7 @@
{% if sub['samples'] %}
<h3><u>Samples:</u></h3>
<p>{% for item in sub['samples'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b><a class="data-link" id="{{ item['submitter_id'] }}_alpha">{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}</a><br>
{% endfor %}</p>
{% endif %}
@@ -76,17 +75,29 @@
<br>
</body>
<script>
var backend;
new QWebChannel(qt.webChannelTransport, function (channel) {
backend = channel.objects.backend;
});
{% block script %}
{{ super() }}
{% for sample in sub['samples'] %}
document.getElementById("{{ sample['submitter_id'] }}").addEventListener("dblclick", function(){
document.getElementById("{{ sample['submitter_id'] }}").addEventListener("click", function(){
backend.sample_details("{{ sample['submitter_id'] }}");
});
{% endfor %}
{% for sample in sub['samples'] %}
document.getElementById("{{ sample['submitter_id'] }}_alpha").addEventListener("click", function(){
backend.sample_details("{{ sample['submitter_id'] }}");
});
{% endfor %}
{% for reagent in sub['reagents'] %}
document.getElementById("{{ reagent['lot'] }}").addEventListener("click", function(){
backend.reagent_details("{{ reagent['lot'] }}", "{{ sub['extraction_kit'] }}");
});
{% endfor %}
document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}");
})
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(true);
}, false);
{% endblock %}
</script>
</html>

View File

@@ -27,3 +27,15 @@
visibility: visible;
font-size: large;
}
.data-link {
color: blue;
text-decoration: underline;
text-decoration-color: blue;
}
.data-link:hover {
color: #ff33ff;
text-decoration-color: #ff33ff;
cursor: pointer;
}

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<meta charset="UTF-8">
{% if css %}
<style>
{{ css }}
</style>
{% endif %}
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{% endblock %}
</head>
<body>
{% block body %}
<!--<button type="button" id="back_btn">Back</button>-->
{% endblock %}
</body>
<script>
{% block script %}
var backend;
new QWebChannel(qt.webChannelTransport, function (channel) {
backend = channel.objects.backend;
});
{% endblock %}
</script>
</html>

View File

@@ -1,6 +1,6 @@
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
{% for sample in samples %}
<div class="well" id="{{sample['submitter_id']}}" style="background-color: {{sample['background_color']}};
<div class="well data-link" id="{{sample['submitter_id']}}" style="background-color: {{sample['background_color']}};
border: 1px solid #000;
padding: 20px;
grid-column-start: {{sample['column']}};

View File

@@ -0,0 +1,35 @@
{% extends "details.html" %}
<head>
{% block head %}
{{ super() }}
<title>Reagent Details for {{ reagent['name'] }} - {{ reagent['lot'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Reagent Details for {{ reagent['name'] }} - {{ reagent['lot'] }}</u></h2>
{{ super() }}
<p>{% for key, value in reagent.items() if key not in reagent['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
{% endfor %}</p>
{% if reagent['submissions'] %}<h2>Submissions:</h2>
{% for submission in reagent['submissions'] %}
<p><b><a class="data-link" id="{{ submission }}">{{ submission }}:</a></b> {{ reagent['role'] }}</p>
{% endfor %}
{% endif %}
{% endblock %}
</body>
<script>
{% block script %}
{{ super() }}
{% for submission in reagent['submissions'] %}
document.getElementById("{{ submission }}").addEventListener("click", function(){
backend.submission_details("{{ submission }}");
});
{% endfor %}
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
{% endblock %}
</script>
</html>

View File

@@ -25,7 +25,9 @@ from __init__ import project_path
from configparser import ConfigParser
from tkinter import Tk # from tkinter import Tk for Python 3.x
from tkinter.filedialog import askdirectory
from .error_messaging import parse_error_to_message
# from .error_messaging import parse_exception_to_message
from sqlalchemy.exc import ArgumentError, IntegrityError as sqlalcIntegrityError
logger = logging.getLogger(f"submissions.{__name__}")
@@ -784,7 +786,33 @@ class Result(BaseModel, arbitrary_types_allowed=True):
@classmethod
def set_message(cls, value):
if isinstance(value, Exception):
value = parse_error_to_message(value=value)
value = cls.parse_exception_to_message(value=value)
return value
@classmethod
def parse_exception_to_message(cls, value: Exception) -> str:
"""
Converts an except to a human-readable error message for display.
Args:
value (Exception): Input exception
Returns:
str: Output message for display
"""
match value:
case sqlalcIntegrityError():
origin = value.orig.__str__().lower()
logger.error(f"Exception origin: {origin}")
if "unique constraint failed:" in origin:
field = " ".join(origin.split(".")[1:]).replace("_", " ").upper()
# logger.debug(field)
value = f"{field} doesn't have a unique value.\nIt must be changed."
else:
value = f"Got unknown integrity error: {value}"
case _:
value = f"Got generic error: {value}"
return value
def __repr__(self) -> str:

View File

@@ -1,30 +0,0 @@
from sqlalchemy.exc import ArgumentError, IntegrityError as sqlalcIntegrityError
import logging
logger = logging.getLogger(f"submissions.{__name__}")
def parse_error_to_message(value: Exception):
"""
Converts an except to a human-readable error message for display.
Args:
value (Exception): Input exception
Returns:
str: Output message for display
"""
match value:
case sqlalcIntegrityError():
origin = value.orig.__str__().lower()
logger.error(f"Exception origin: {origin}")
if "unique constraint failed:" in origin:
field = " ".join(origin.split(".")[1:]).replace("_", " ").upper()
# logger.debug(field)
value = f"{field} doesn't have a unique value.\nIt must be changed."
else:
value = f"Got unknown integrity error: {value}"
case _:
value = f"Got generic error: {value}"
return value