Improved navigation and clarity in details view.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
## 202409.03
|
## 202409.03
|
||||||
|
|
||||||
|
- Better navigation and clarity in details panes.
|
||||||
- Upgraded sample search to (semi) realtime search.
|
- Upgraded sample search to (semi) realtime search.
|
||||||
- Improved error messaging.
|
- Improved error messaging.
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import json
|
|||||||
from pprint import pprint, pformat
|
from pprint import pprint, pformat
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from jinja2 import TemplateNotFound
|
||||||
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
||||||
from sqlalchemy.orm import relationship, validates, Query
|
from sqlalchemy.orm import relationship, validates, Query
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from datetime import date
|
from datetime import date
|
||||||
import logging, re
|
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 typing import List, Literal, Generator, Any
|
||||||
from pandas import ExcelFile
|
from pandas import ExcelFile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -421,12 +422,13 @@ class Reagent(BaseClass):
|
|||||||
else:
|
else:
|
||||||
return f"<Reagent({self.role.name}-{self.lot})>"
|
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
|
dictionary containing values necessary for gui
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
extraction_kit (KitType, optional): KitType to use to get reagent type. Defaults to None.
|
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:
|
Returns:
|
||||||
dict: representation of the reagent's attributes
|
dict: representation of the reagent's attributes
|
||||||
@@ -456,13 +458,17 @@ class Reagent(BaseClass):
|
|||||||
place_holder = "NA"
|
place_holder = "NA"
|
||||||
else:
|
else:
|
||||||
place_holder = place_holder.strftime("%Y-%m-%d")
|
place_holder = place_holder.strftime("%Y-%m-%d")
|
||||||
return dict(
|
output = dict(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
role=rtype,
|
role=rtype,
|
||||||
lot=self.lot,
|
lot=self.lot,
|
||||||
expiry=place_holder,
|
expiry=place_holder,
|
||||||
missing=False
|
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:
|
def update_last_used(self, kit: KitType) -> Report:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ class BasicSubmission(BaseClass):
|
|||||||
'extraction_info', 'comment', 'barcode',
|
'extraction_info', 'comment', 'barcode',
|
||||||
'platemap', 'export_map', 'equipment', 'tips'],
|
'platemap', 'export_map', 'equipment', 'tips'],
|
||||||
# NOTE: Fields not placed in ui form
|
# 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
|
# NOTE: Fields not placed in ui form to be moved to pydantic
|
||||||
form_recover=recover
|
form_recover=recover
|
||||||
)
|
)
|
||||||
@@ -229,7 +230,8 @@ class BasicSubmission(BaseClass):
|
|||||||
return SubmissionType.query(cls.__mapper_args__['polymorphic_identity'])
|
return SubmissionType.query(cls.__mapper_args__['polymorphic_identity'])
|
||||||
|
|
||||||
@classmethod
|
@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.
|
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)
|
return cls.get_submission_type(submission_type).construct_info_map(mode=mode)
|
||||||
|
|
||||||
@classmethod
|
@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
|
Method to call submission type's construct_sample_map
|
||||||
|
|
||||||
@@ -609,7 +611,7 @@ class BasicSubmission(BaseClass):
|
|||||||
new_dict = {}
|
new_dict = {}
|
||||||
for key, value in dicto.items():
|
for key, value in dicto.items():
|
||||||
# logger.debug(f"Checking {key}")
|
# logger.debug(f"Checking {key}")
|
||||||
missing = value is None or value in ['', 'None']
|
missing = value in ['', 'None', None]
|
||||||
match key:
|
match key:
|
||||||
case "reagents":
|
case "reagents":
|
||||||
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
||||||
@@ -628,7 +630,7 @@ class BasicSubmission(BaseClass):
|
|||||||
case "id":
|
case "id":
|
||||||
pass
|
pass
|
||||||
case _:
|
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)
|
new_dict[key.lower().replace(" ", "_")] = dict(value=value, missing=missing)
|
||||||
# logger.debug(f"{key} complete after {time()-start}")
|
# logger.debug(f"{key} complete after {time()-start}")
|
||||||
new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
|
new_dict['filepath'] = Path(tempfile.TemporaryFile().name)
|
||||||
@@ -648,7 +650,7 @@ class BasicSubmission(BaseClass):
|
|||||||
return super().save()
|
return super().save()
|
||||||
|
|
||||||
@classmethod
|
@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 {cls.__mapper_args__['polymorphic_identity']}")
|
||||||
logger.debug(f"Attempting to get regex for {submission_type}")
|
logger.debug(f"Attempting to get regex for {submission_type}")
|
||||||
try:
|
try:
|
||||||
@@ -707,7 +709,8 @@ class BasicSubmission(BaseClass):
|
|||||||
# item.__mapper_args__['polymorphic_identity'] == polymorphic_identity][0]
|
# item.__mapper_args__['polymorphic_identity'] == polymorphic_identity][0]
|
||||||
model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_
|
model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_
|
||||||
except Exception as e:
|
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 _:
|
case _:
|
||||||
pass
|
pass
|
||||||
if attrs is None or len(attrs) == 0:
|
if attrs is None or len(attrs) == 0:
|
||||||
@@ -1199,6 +1202,8 @@ class BasicSubmission(BaseClass):
|
|||||||
dlg = SubmissionComment(parent=obj, submission=self)
|
dlg = SubmissionComment(parent=obj, submission=self)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
comment = dlg.parse_form()
|
comment = dlg.parse_form()
|
||||||
|
if comment in ["", None]:
|
||||||
|
return
|
||||||
self.set_attribute(key='comment', value=comment)
|
self.set_attribute(key='comment', value=comment)
|
||||||
# logger.debug(self.comment)
|
# logger.debug(self.comment)
|
||||||
self.save(original=False)
|
self.save(original=False)
|
||||||
@@ -1368,49 +1373,6 @@ class BacterialCulture(BasicSubmission):
|
|||||||
return input_dict
|
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):
|
class Wastewater(BasicSubmission):
|
||||||
"""
|
"""
|
||||||
derivative submission type from BasicSubmission
|
derivative submission type from BasicSubmission
|
||||||
@@ -2239,6 +2201,9 @@ class BasicSample(BaseClass):
|
|||||||
"""
|
"""
|
||||||
gui friendly dictionary, extends parent method.
|
gui friendly dictionary, extends parent method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_data (bool): Whether to use full object or truncated. Defaults to False
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ class ReportDatePicker(QDialog):
|
|||||||
"""
|
"""
|
||||||
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
||||||
|
|
||||||
|
|
||||||
class LogParser(QDialog):
|
class LogParser(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Webview to show submission and sample details.
|
Webview to show submission and sample details.
|
||||||
"""
|
"""
|
||||||
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
|
from PyQt6.QtGui import QColor
|
||||||
QDialogButtonBox, QTextEdit)
|
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
|
||||||
|
QDialogButtonBox, QTextEdit, QGridLayout)
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWebChannel import QWebChannel
|
from PyQt6.QtWebChannel import QWebChannel
|
||||||
from PyQt6.QtCore import Qt, pyqtSlot
|
from PyQt6.QtCore import Qt, pyqtSlot
|
||||||
|
from jinja2 import TemplateNotFound
|
||||||
|
|
||||||
from backend.db.models import BasicSubmission, BasicSample
|
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
|
||||||
from tools import is_power_user, html_to_pdf
|
from tools import is_power_user, html_to_pdf, jinja_template_loading
|
||||||
from .functions import select_save_file
|
from .functions import select_save_file
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
@@ -25,8 +27,9 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
|||||||
class SubmissionDetails(QDialog):
|
class SubmissionDetails(QDialog):
|
||||||
"""
|
"""
|
||||||
a window showing text details of submission
|
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)
|
super().__init__(parent)
|
||||||
try:
|
try:
|
||||||
@@ -35,15 +38,20 @@ class SubmissionDetails(QDialog):
|
|||||||
self.app = None
|
self.app = None
|
||||||
self.webview = QWebEngineView(parent=self)
|
self.webview = QWebEngineView(parent=self)
|
||||||
self.webview.setMinimumSize(900, 500)
|
self.webview.setMinimumSize(900, 500)
|
||||||
self.webview.setMaximumSize(900, 500)
|
self.webview.setMaximumSize(900, 700)
|
||||||
self.layout = QVBoxLayout()
|
self.webview.loadFinished.connect(self.activate_export)
|
||||||
self.setFixedSize(900, 500)
|
self.layout = QGridLayout()
|
||||||
|
# self.setFixedSize(900, 500)
|
||||||
# NOTE: button to export a pdf version
|
# NOTE: button to export a pdf version
|
||||||
btn = QPushButton("Export DOCX")
|
self.btn = QPushButton("Export DOCX")
|
||||||
btn.setFixedWidth(875)
|
self.btn.setFixedWidth(775)
|
||||||
btn.clicked.connect(self.export)
|
self.btn.clicked.connect(self.export)
|
||||||
self.layout.addWidget(btn)
|
self.back = QPushButton("Back")
|
||||||
self.layout.addWidget(self.webview)
|
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)
|
self.setLayout(self.layout)
|
||||||
# NOTE: setup channel
|
# NOTE: setup channel
|
||||||
self.channel = QWebChannel()
|
self.channel = QWebChannel()
|
||||||
@@ -54,10 +62,26 @@ class SubmissionDetails(QDialog):
|
|||||||
self.rsl_plate_num = sub.rsl_plate_num
|
self.rsl_plate_num = sub.rsl_plate_num
|
||||||
case BasicSample():
|
case BasicSample():
|
||||||
self.sample_details(sample=sub)
|
self.sample_details(sample=sub)
|
||||||
|
case Reagent():
|
||||||
|
self.reagent_details(reagent=sub)
|
||||||
self.webview.page().setWebChannel(self.channel)
|
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)
|
@pyqtSlot(str)
|
||||||
def sample_details(self, sample:str|BasicSample):
|
def sample_details(self, sample: str | BasicSample):
|
||||||
"""
|
"""
|
||||||
Changes details view to summary of Sample
|
Changes details view to summary of Sample
|
||||||
|
|
||||||
@@ -68,21 +92,49 @@ class SubmissionDetails(QDialog):
|
|||||||
sample = BasicSample.query(submitter_id=sample)
|
sample = BasicSample.query(submitter_id=sample)
|
||||||
base_dict = sample.to_sub_dict(full_data=True)
|
base_dict = sample.to_sub_dict(full_data=True)
|
||||||
base_dict, template = sample.get_details_template(base_dict=base_dict)
|
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.webview.setHtml(html)
|
||||||
self.setWindowTitle(f"Sample Details - {sample.submitter_id}")
|
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)
|
@pyqtSlot(str)
|
||||||
def submission_details(self, submission:str|BasicSubmission):
|
def submission_details(self, submission: str | BasicSubmission):
|
||||||
"""
|
"""
|
||||||
Sets details view to summary of Submission.
|
Sets details view to summary of Submission.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
submission (str | BasicSubmission): Submission of interest.
|
submission (str | BasicSubmission): Submission of interest.
|
||||||
"""
|
"""
|
||||||
# logger.debug(f"Details for: {submission}")
|
# logger.debug(f"Details for: {submission}")
|
||||||
if isinstance(submission, str):
|
if isinstance(submission, str):
|
||||||
submission = BasicSubmission.query(rsl_plate_num=submission)
|
submission = BasicSubmission.query(rsl_plate_num=submission)
|
||||||
|
self.rsl_plate_num = submission.rsl_plate_num
|
||||||
self.base_dict = submission.to_dict(full_data=True)
|
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'})}")
|
# 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
|
# NOTE: don't want id
|
||||||
@@ -97,10 +149,10 @@ class SubmissionDetails(QDialog):
|
|||||||
# logger.debug(f"Submission_details: {pformat(self.base_dict)}")
|
# 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.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css)
|
||||||
self.webview.setHtml(self.html)
|
self.webview.setHtml(self.html)
|
||||||
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
|
# self.btn.setEnabled(True)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def sign_off(self, submission:str|BasicSubmission):
|
def sign_off(self, submission: str | BasicSubmission):
|
||||||
# logger.debug(f"Signing off on {submission} - ({getuser()})")
|
# logger.debug(f"Signing off on {submission} - ({getuser()})")
|
||||||
if isinstance(submission, str):
|
if isinstance(submission, str):
|
||||||
submission = BasicSubmission.query(rsl_plate_num=submission)
|
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.
|
Renders submission to html, then creates and saves .pdf file to user selected file.
|
||||||
"""
|
"""
|
||||||
writer = DocxWriter(base_dict=self.base_dict)
|
export_plate = BasicSubmission.query(rsl_plate_num=self.export_plate)
|
||||||
fname = select_save_file(obj=self, default_name=self.base_dict['plate_number'], extension="docx")
|
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)
|
writer.save(fname)
|
||||||
try:
|
try:
|
||||||
html_to_pdf(html=self.html, output_file=fname)
|
html_to_pdf(html=self.html, output_file=fname)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
logger.error(f"Error saving pdf: {e}")
|
logger.error(f"Error saving pdf: {e}")
|
||||||
|
|
||||||
|
|
||||||
class SubmissionComment(QDialog):
|
class SubmissionComment(QDialog):
|
||||||
"""
|
"""
|
||||||
a window for adding comment text to a submission
|
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)
|
super().__init__(parent)
|
||||||
try:
|
try:
|
||||||
@@ -137,7 +193,8 @@ class SubmissionComment(QDialog):
|
|||||||
# NOTE: create text field
|
# NOTE: create text field
|
||||||
self.txt_editor = QTextEdit(self)
|
self.txt_editor = QTextEdit(self)
|
||||||
self.txt_editor.setReadOnly(False)
|
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
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
@@ -147,14 +204,16 @@ class SubmissionComment(QDialog):
|
|||||||
self.layout.addWidget(self.txt_editor)
|
self.layout.addWidget(self.txt_editor)
|
||||||
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
|
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def parse_form(self) -> List[dict]:
|
def parse_form(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Adds comment to submission object.
|
Adds comment to submission object.
|
||||||
"""
|
"""
|
||||||
commenter = getuser()
|
commenter = getuser()
|
||||||
comment = self.txt_editor.toPlainText()
|
comment = self.txt_editor.toPlainText()
|
||||||
|
if comment in ["", None]:
|
||||||
|
return None
|
||||||
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
|
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}")
|
# logger.debug(f"Full comment: {full_comment}")
|
||||||
return full_comment
|
return full_comment
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ class SubmissionsSheet(QTableView):
|
|||||||
dlg = ReportDatePicker()
|
dlg = ReportDatePicker()
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
info = dlg.parse_form()
|
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 = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
|
||||||
rp.write_report(filename=fname, obj=self)
|
rp.write_report(filename=fname, obj=self)
|
||||||
return report
|
return report
|
||||||
|
|||||||
@@ -191,7 +191,9 @@ class SubmissionFormWidget(QWidget):
|
|||||||
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
|
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
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:
|
if k in self.ignore:
|
||||||
|
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
# logger.debug(f"Key: {k}, Disable: {disable}")
|
# logger.debug(f"Key: {k}, Disable: {disable}")
|
||||||
@@ -201,9 +203,12 @@ class SubmissionFormWidget(QWidget):
|
|||||||
check = False
|
check = False
|
||||||
try:
|
try:
|
||||||
value = self.pyd.__getattribute__(k)
|
value = self.pyd.__getattribute__(k)
|
||||||
except AttributeError:
|
except AttributeError as e:
|
||||||
logger.error(f"Couldn't get attribute from pyd: {k}")
|
logger.error(f"Couldn't get attribute from pyd: {k} due to {e}")
|
||||||
value = dict(value=None, missing=True)
|
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'],
|
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'],
|
||||||
sub_obj=st, disable=check)
|
sub_obj=st, disable=check)
|
||||||
if add_widget is not None:
|
if add_widget is not None:
|
||||||
@@ -259,7 +264,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
Tuple[QMainWindow, dict]: Updated application and result
|
Tuple[QMainWindow, dict]: Updated application and result
|
||||||
"""
|
"""
|
||||||
extraction_kit = args[0]
|
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"Self.reagents: {self.reagents}")
|
||||||
# logger.debug(f"\n\n{pformat(caller)}\n\n")
|
# logger.debug(f"\n\n{pformat(caller)}\n\n")
|
||||||
# logger.debug(f"SubmissionType: {self.submission_type}")
|
# logger.debug(f"SubmissionType: {self.submission_type}")
|
||||||
|
|||||||
@@ -1,65 +1,37 @@
|
|||||||
<!doctype html>
|
{% extends "details.html" %}
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
{{ super() }}
|
||||||
/* Tooltip container */
|
<title>Sample Details for {{ sample['submitter_id'] }}</title>
|
||||||
.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>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block 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'] %}
|
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
|
||||||
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% if sample['submissions'] %}<h2>Submissions:</h2>
|
{% if sample['submissions'] %}<h2>Submissions:</h2>
|
||||||
{% for submission in sample['submissions'] %}
|
{% 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 %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
var backend;
|
{% block script %}
|
||||||
new QWebChannel(qt.webChannelTransport, function (channel) {
|
{{ super() }}
|
||||||
backend = channel.objects.backend;
|
|
||||||
});
|
|
||||||
{% for submission in sample['submissions'] %}
|
{% 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'] }}");
|
backend.submission_details("{{ submission['plate_name'] }}");
|
||||||
});
|
});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
backend.activate_export(false);
|
||||||
|
}, false);
|
||||||
|
{% endblock %}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
<!doctype html>
|
{% extends "details.html" %}
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% if css %}
|
{{ super() }}
|
||||||
<style>
|
<title>Submission Details for {{ sub['plate_number'] }}</title>
|
||||||
{{ css }}
|
|
||||||
</style>
|
|
||||||
{% endif %}
|
|
||||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
|
||||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2><u>Submission Details for {{ sub['plate_number'] }}</u></h2> {% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
|
<h2><u>Submission Details for {{ sub['plate_number'] }}</u></h2> {% 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'] %}
|
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
|
||||||
<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
|
|
||||||
<h3><u>Reagents:</u></h3>
|
<h3><u>Reagents:</u></h3>
|
||||||
<p>{% for item in sub['reagents'] %}
|
<p>{% for item in sub['reagents'] %}
|
||||||
<b>{{ item['role'] }}</b>: {{ item['lot'] }} (EXP: {{ item['expiry'] }})<br>
|
<b>{{ item['role'] }}</b>: <a class="data-link" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
|
|
||||||
{% if sub['equipment'] %}
|
{% if sub['equipment'] %}
|
||||||
<h3><u>Equipment:</u></h3>
|
<h3><u>Equipment:</u></h3>
|
||||||
<p>{% for item in sub['equipment'] %}
|
<p>{% for item in sub['equipment'] %}
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
{% if sub['samples'] %}
|
{% if sub['samples'] %}
|
||||||
<h3><u>Samples:</u></h3>
|
<h3><u>Samples:</u></h3>
|
||||||
<p>{% for item in sub['samples'] %}
|
<p>{% for item in sub['samples'] %}
|
||||||
<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
<b>{{ item['well'] }}:</b><a class="data-link" id="{{ item['submitter_id'] }}_alpha">{% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% endif %}</a><br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -76,17 +75,29 @@
|
|||||||
<br>
|
<br>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
var backend;
|
{% block script %}
|
||||||
new QWebChannel(qt.webChannelTransport, function (channel) {
|
{{ super() }}
|
||||||
backend = channel.objects.backend;
|
|
||||||
});
|
|
||||||
{% for sample in sub['samples'] %}
|
{% 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'] }}");
|
backend.sample_details("{{ sample['submitter_id'] }}");
|
||||||
});
|
});
|
||||||
{% endfor %}
|
{% 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(){
|
document.getElementById("sign_btn").addEventListener("click", function(){
|
||||||
backend.sign_off("{{ sub['plate_number'] }}");
|
backend.sign_off("{{ sub['plate_number'] }}");
|
||||||
})
|
})
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
backend.activate_export(true);
|
||||||
|
}, false);
|
||||||
|
{% endblock %}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -27,3 +27,15 @@
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
font-size: large;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
28
src/submissions/templates/details.html
Normal file
28
src/submissions/templates/details.html
Normal 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>
|
||||||
@@ -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;">
|
<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 %}
|
{% 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;
|
border: 1px solid #000;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
grid-column-start: {{sample['column']}};
|
grid-column-start: {{sample['column']}};
|
||||||
|
|||||||
35
src/submissions/templates/reagent_details.html
Normal file
35
src/submissions/templates/reagent_details.html
Normal 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'] %}
|
||||||
|
<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>
|
||||||
@@ -25,7 +25,9 @@ from __init__ import project_path
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from tkinter import Tk # from tkinter import Tk for Python 3.x
|
from tkinter import Tk # from tkinter import Tk for Python 3.x
|
||||||
from tkinter.filedialog import askdirectory
|
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__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -784,7 +786,33 @@ class Result(BaseModel, arbitrary_types_allowed=True):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def set_message(cls, value):
|
def set_message(cls, value):
|
||||||
if isinstance(value, Exception):
|
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
|
return value
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user