Prior to moving settings to pydantic-settings
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
## 202307.04
|
||||||
|
|
||||||
|
- Individual plate details now in html format.
|
||||||
|
|
||||||
## 202307.03
|
## 202307.03
|
||||||
|
|
||||||
- Auto-filling of some empty cells in excel file.
|
- Auto-filling of some empty cells in excel file.
|
||||||
|
|||||||
@@ -6,15 +6,16 @@
|
|||||||
## Logging in New Run:
|
## Logging in New Run:
|
||||||
*should fit 90% of usage cases*
|
*should fit 90% of usage cases*
|
||||||
|
|
||||||
1. Ensure a properly formatted Submission Excel form has been filled out. (It will save you a few headaches)
|
1. Ensure a properly formatted Submission Excel form has been filled out.
|
||||||
a. All fields should be filled in to ensure proper lookups of reagents.
|
a. The program can fill in reagent fields and some other information automatically, but should be checked for accuracy afterwards.
|
||||||
2. Click on 'File' in the menu bar, followed by 'Import Submission' and use the file dialog to locate the form you definitely made sure was properly filled out in step 1.
|
2. Click on 'File' in the menu bar, followed by 'Import Submission' and use the file dialog to locate the form.
|
||||||
3. Click 'Ok'.
|
3. Click 'Ok'.
|
||||||
4. Most of the fields in the form should be automatically filled in from the form area to the left of the screen.
|
4. Most of the fields in the form should be automatically filled in from the form area to the left of the screen.
|
||||||
5. You may need to maximize the app to ensure you can see all the info.
|
5. You may need to maximize the app to ensure you can see all the info.
|
||||||
6. Any fields that are not automatically filled in can be filled in manually from the drop down menus.
|
6. Any fields that are not automatically filled in can be filled in manually from the drop down menus.
|
||||||
|
a. Any reagent lots not found in the drop downs can be typed in manually.
|
||||||
7. Once you are certain all the information is correct, click 'Submit' at the bottom of the form.
|
7. Once you are certain all the information is correct, click 'Submit' at the bottom of the form.
|
||||||
8. Add in any reagents the app doesn't recognize.
|
8. Add in any new reagents the app doesn't have in the database.
|
||||||
9. Once the new run shows up at the bottom of the Submissions, everything is fine.
|
9. Once the new run shows up at the bottom of the Submissions, everything is fine.
|
||||||
10. In case of any mistakes, the run can be overwritten by a reimport.
|
10. In case of any mistakes, the run can be overwritten by a reimport.
|
||||||
|
|
||||||
|
|||||||
3
TODO.md
3
TODO.md
@@ -1,3 +1,6 @@
|
|||||||
|
- [ ] Check robotics' use of Artic submission forms (i.e. will we be able to make our own forms?)
|
||||||
|
- [ ] Streamline addition of new kits by moving as much into DB as possible.
|
||||||
|
- [x] Make plate details from html, same as export.
|
||||||
- [x] Put in SN controls I guess.
|
- [x] Put in SN controls I guess.
|
||||||
- [x] Code clean-up and refactor (2023-07).
|
- [x] Code clean-up and refactor (2023-07).
|
||||||
- [ ] Migrate context settings to pydantic-settings model.
|
- [ ] Migrate context settings to pydantic-settings model.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Version of the realpython-reader package
|
# Version of the realpython-reader package
|
||||||
__project__ = "submissions"
|
__project__ = "submissions"
|
||||||
__version__ = "202307.3b"
|
__version__ = "202307.4b"
|
||||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||||
__copyright__ = "2022-2023, Government of Canada"
|
__copyright__ = "2022-2023, Government of Canada"
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ from PyQt6.QtWidgets import (
|
|||||||
QMessageBox, QFileDialog, QMenu, QLabel,
|
QMessageBox, QFileDialog, QMenu, QLabel,
|
||||||
QDialogButtonBox, QToolBar
|
QDialogButtonBox, QToolBar
|
||||||
)
|
)
|
||||||
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||||
from PyQt6.QtGui import QFontMetrics, QAction, QCursor, QPixmap, QPainter
|
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
||||||
from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id, lookup_submission_by_rsl_num, hitpick_plate
|
from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id, lookup_submission_by_rsl_num, hitpick_plate
|
||||||
from backend.excel import make_hitpicks
|
from backend.excel import make_hitpicks
|
||||||
from configure import jinja_template_loading
|
from configure import jinja_template_loading
|
||||||
@@ -266,40 +267,25 @@ class SubmissionDetails(QDialog):
|
|||||||
# don't want id
|
# don't want id
|
||||||
del self.base_dict['id']
|
del self.base_dict['id']
|
||||||
# retrieve jinja template
|
# retrieve jinja template
|
||||||
template = env.get_template("submission_details.txt")
|
# template = env.get_template("submission_details.txt")
|
||||||
# render using object dict
|
# render using object dict
|
||||||
text = template.render(sub=self.base_dict)
|
# text = template.render(sub=self.base_dict)
|
||||||
# create text field
|
# create text field
|
||||||
txt_editor = QTextEdit(self)
|
# txt_editor = QTextEdit(self)
|
||||||
txt_editor.setReadOnly(True)
|
# txt_editor.setReadOnly(True)
|
||||||
txt_editor.document().setPlainText(text)
|
# txt_editor.document().setPlainText(text)
|
||||||
# resize
|
# resize
|
||||||
font = txt_editor.document().defaultFont()
|
# font = txt_editor.document().defaultFont()
|
||||||
fontMetrics = QFontMetrics(font)
|
# fontMetrics = QFontMetrics(font)
|
||||||
textSize = fontMetrics.size(0, txt_editor.toPlainText())
|
# textSize = fontMetrics.size(0, txt_editor.toPlainText())
|
||||||
w = textSize.width() + 10
|
# w = textSize.width() + 10
|
||||||
h = textSize.height() + 10
|
# h = textSize.height() + 10
|
||||||
txt_editor.setMinimumSize(w, h)
|
# txt_editor.setMinimumSize(w, h)
|
||||||
txt_editor.setMaximumSize(w, h)
|
# txt_editor.setMaximumSize(w, h)
|
||||||
txt_editor.resize(w, h)
|
# txt_editor.resize(w, h)
|
||||||
interior.resize(w,900)
|
# interior.resize(w,900)
|
||||||
txt_editor.setText(text)
|
# txt_editor.setText(text)
|
||||||
interior.setWidget(txt_editor)
|
# interior.setWidget(txt_editor)
|
||||||
self.layout = QVBoxLayout()
|
|
||||||
self.setFixedSize(w, 900)
|
|
||||||
# button to export a pdf version
|
|
||||||
btn = QPushButton("Export PDF")
|
|
||||||
btn.setParent(self)
|
|
||||||
btn.setFixedWidth(w)
|
|
||||||
btn.clicked.connect(self.export)
|
|
||||||
|
|
||||||
|
|
||||||
def export(self):
|
|
||||||
"""
|
|
||||||
Renders submission to html, then creates and saves .pdf file to user selected file.
|
|
||||||
"""
|
|
||||||
template = env.get_template("submission_details.html")
|
|
||||||
# make barcode because, reasons
|
|
||||||
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
|
self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
|
||||||
sub = lookup_submission_by_rsl_num(ctx=self.ctx, rsl_num=self.base_dict['Plate Number'])
|
sub = lookup_submission_by_rsl_num(ctx=self.ctx, rsl_num=self.base_dict['Plate Number'])
|
||||||
plate_dicto = hitpick_plate(sub)
|
plate_dicto = hitpick_plate(sub)
|
||||||
@@ -312,10 +298,45 @@ class SubmissionDetails(QDialog):
|
|||||||
logger.error(f"No plate map found for {sub.rsl_plate_num}")
|
logger.error(f"No plate map found for {sub.rsl_plate_num}")
|
||||||
# platemap.save("test.jpg", 'JPEG')
|
# platemap.save("test.jpg", 'JPEG')
|
||||||
self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
||||||
logger.debug(self.base_dict)
|
template = env.get_template("submission_details.html")
|
||||||
html = template.render(sub=self.base_dict)
|
self.html = template.render(sub=self.base_dict)
|
||||||
with open("test.html", "w") as f:
|
webview = QWebEngineView()
|
||||||
f.write(html)
|
webview.setMinimumSize(900, 500)
|
||||||
|
webview.setMaximumSize(900, 500)
|
||||||
|
webview.setHtml(self.html)
|
||||||
|
self.layout = QVBoxLayout()
|
||||||
|
interior.resize(900, 500)
|
||||||
|
interior.setWidget(webview)
|
||||||
|
self.setFixedSize(900, 500)
|
||||||
|
# button to export a pdf version
|
||||||
|
btn = QPushButton("Export PDF")
|
||||||
|
btn.setParent(self)
|
||||||
|
btn.setFixedWidth(900)
|
||||||
|
btn.clicked.connect(self.export)
|
||||||
|
|
||||||
|
|
||||||
|
def export(self):
|
||||||
|
"""
|
||||||
|
Renders submission to html, then creates and saves .pdf file to user selected file.
|
||||||
|
"""
|
||||||
|
# template = env.get_template("submission_details.html")
|
||||||
|
# # make barcode because, reasons
|
||||||
|
# self.base_dict['barcode'] = base64.b64encode(make_plate_barcode(self.base_dict['Plate Number'], width=120, height=30)).decode('utf-8')
|
||||||
|
# sub = lookup_submission_by_rsl_num(ctx=self.ctx, rsl_num=self.base_dict['Plate Number'])
|
||||||
|
# plate_dicto = hitpick_plate(sub)
|
||||||
|
# platemap = make_plate_map(plate_dicto)
|
||||||
|
# logger.debug(f"platemap: {platemap}")
|
||||||
|
# image_io = BytesIO()
|
||||||
|
# try:
|
||||||
|
# platemap.save(image_io, 'JPEG')
|
||||||
|
# except AttributeError:
|
||||||
|
# logger.error(f"No plate map found for {sub.rsl_plate_num}")
|
||||||
|
# # platemap.save("test.jpg", 'JPEG')
|
||||||
|
# self.base_dict['platemap'] = base64.b64encode(image_io.getvalue()).decode('utf-8')
|
||||||
|
# logger.debug(self.base_dict)
|
||||||
|
# html = template.render(sub=self.base_dict)
|
||||||
|
# with open("test.html", "w") as f:
|
||||||
|
# f.write(html)
|
||||||
try:
|
try:
|
||||||
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
|
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -326,7 +347,7 @@ class SubmissionDetails(QDialog):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
with open(fname, "w+b") as f:
|
with open(fname, "w+b") as f:
|
||||||
pisa.CreatePDF(html, dest=f)
|
pisa.CreatePDF(self.html, dest=f)
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
logger.error(f"Error saving pdf: {e}")
|
logger.error(f"Error saving pdf: {e}")
|
||||||
msg = QMessageBox()
|
msg = QMessageBox()
|
||||||
|
|||||||
@@ -816,7 +816,7 @@ def import_pcr_results_function(obj:QMainWindow) -> Tuple[QMainWindow, dict]:
|
|||||||
|
|
||||||
def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_reagents:List[str], info:dict):
|
def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_reagents:List[str], info:dict):
|
||||||
"""
|
"""
|
||||||
Automatically fills in excel cells with reagent info.
|
Automatically fills in excel cells with submission info.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (QMainWindow): Original main app window
|
obj (QMainWindow): Original main app window
|
||||||
@@ -829,13 +829,15 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
|
|||||||
|
|
||||||
logger.debug(f"Here is the info dict coming in:\n{pprint.pformat(info)}")
|
logger.debug(f"Here is the info dict coming in:\n{pprint.pformat(info)}")
|
||||||
logger.debug(f"Here are the missing reagents:\n{missing_reagents}")
|
logger.debug(f"Here are the missing reagents:\n{missing_reagents}")
|
||||||
|
# pare down the xl map to only the missing data.
|
||||||
relevant_map = {k:v for k,v in xl_map.items() if k in missing_reagents}
|
relevant_map = {k:v for k,v in xl_map.items() if k in missing_reagents}
|
||||||
# logger.debug(relevant_map)
|
# pare down reagents to only what's missing
|
||||||
relevant_reagents = [item for item in reagents if item['type'] in missing_reagents]
|
relevant_reagents = [item for item in reagents if item['type'] in missing_reagents]
|
||||||
|
# hacky manipulation of submission type so it looks better.
|
||||||
info['submission_type'] = info['submission_type'].replace("_", " ").title()
|
info['submission_type'] = info['submission_type'].replace("_", " ").title()
|
||||||
|
# pare down info to just what's missing
|
||||||
relevant_info = {k:v for k,v in info.items() if k in missing_reagents}
|
relevant_info = {k:v for k,v in info.items() if k in missing_reagents}
|
||||||
logger.debug(f"Here is the relevant info: {pprint.pformat(relevant_info)}")
|
logger.debug(f"Here is the relevant info: {pprint.pformat(relevant_info)}")
|
||||||
# logger.debug(f"Relevant reagents:\n{relevant_reagents}")
|
|
||||||
# construct new objects to put into excel sheets:
|
# construct new objects to put into excel sheets:
|
||||||
new_reagents = []
|
new_reagents = []
|
||||||
for reagent in relevant_reagents:
|
for reagent in relevant_reagents:
|
||||||
@@ -846,12 +848,14 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
|
|||||||
new_reagent['expiry'] = relevant_map[new_reagent['type']]['expiry']
|
new_reagent['expiry'] = relevant_map[new_reagent['type']]['expiry']
|
||||||
new_reagent['expiry']['value'] = reagent['expiry']
|
new_reagent['expiry']['value'] = reagent['expiry']
|
||||||
new_reagent['sheet'] = relevant_map[new_reagent['type']]['sheet']
|
new_reagent['sheet'] = relevant_map[new_reagent['type']]['sheet']
|
||||||
|
# name is only present for Bacterial Culture
|
||||||
try:
|
try:
|
||||||
new_reagent['name'] = relevant_map[new_reagent['type']]['name']
|
new_reagent['name'] = relevant_map[new_reagent['type']]['name']
|
||||||
new_reagent['name']['value'] = reagent['type']
|
new_reagent['name']['value'] = reagent['type']
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
new_reagents.append(new_reagent)
|
new_reagents.append(new_reagent)
|
||||||
|
# construct new info objects to put into excel sheets
|
||||||
new_info = []
|
new_info = []
|
||||||
for item in relevant_info:
|
for item in relevant_info:
|
||||||
new_item = {}
|
new_item = {}
|
||||||
@@ -860,11 +864,15 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
|
|||||||
new_item['value'] = relevant_info[item]
|
new_item['value'] = relevant_info[item]
|
||||||
new_info.append(new_item)
|
new_info.append(new_item)
|
||||||
logger.debug(f"New reagents: {new_reagents}")
|
logger.debug(f"New reagents: {new_reagents}")
|
||||||
|
# open the workbook using openpyxl
|
||||||
workbook = load_workbook(obj.xl)
|
workbook = load_workbook(obj.xl)
|
||||||
|
# get list of sheet names
|
||||||
sheets = workbook.sheetnames
|
sheets = workbook.sheetnames
|
||||||
logger.debug(workbook.sheetnames)
|
# logger.debug(workbook.sheetnames)
|
||||||
for sheet in sheets:
|
for sheet in sheets:
|
||||||
|
# open sheet
|
||||||
worksheet=workbook[sheet]
|
worksheet=workbook[sheet]
|
||||||
|
# Get relevant reagents for that sheet
|
||||||
sheet_reagents = [item for item in new_reagents if sheet in item['sheet']]
|
sheet_reagents = [item for item in new_reagents if sheet in item['sheet']]
|
||||||
for reagent in sheet_reagents:
|
for reagent in sheet_reagents:
|
||||||
logger.debug(f"Attempting: {reagent['type']}:")
|
logger.debug(f"Attempting: {reagent['type']}:")
|
||||||
@@ -874,10 +882,12 @@ def autofill_excel(obj:QMainWindow, xl_map:dict, reagents:List[dict], missing_re
|
|||||||
worksheet.cell(row=reagent['name']['row'], column=reagent['name']['column'], value=reagent['name']['value'].replace("_", " ").upper())
|
worksheet.cell(row=reagent['name']['row'], column=reagent['name']['column'], value=reagent['name']['value'].replace("_", " ").upper())
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
# Get relevant info for that sheet
|
||||||
sheet_info = [item for item in new_info if sheet in item['location']['sheets']]
|
sheet_info = [item for item in new_info if sheet in item['location']['sheets']]
|
||||||
for item in sheet_info:
|
for item in sheet_info:
|
||||||
logger.debug(f"Attempting: {item['type']}")
|
logger.debug(f"Attempting: {item['type']}")
|
||||||
worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value'])
|
worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value'])
|
||||||
|
# Hacky way to
|
||||||
if info['submission_type'] == "Bacterial Culture":
|
if info['submission_type'] == "Bacterial Culture":
|
||||||
workbook["Sample List"].cell(row=14, column=2, value=getuser())
|
workbook["Sample List"].cell(row=14, column=2, value=getuser())
|
||||||
fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx")
|
fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx")
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
{# template for constructing submission details #}
|
|
||||||
{% 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 %}
|
|
||||||
{% endfor %}
|
|
||||||
Reagents:
|
|
||||||
{% for item in sub['reagents'] %}
|
|
||||||
{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }}){% endfor %}
|
|
||||||
{% if sub['samples']%}
|
|
||||||
Samples:
|
|
||||||
{% for item in sub['samples'] %}
|
|
||||||
{{ item['well'] }}: {{ item['name'] }}{% endfor %}{% endif %}
|
|
||||||
{% if sub['controls'] %}
|
|
||||||
Attached Controls:
|
|
||||||
{% for item in sub['controls'] %}
|
|
||||||
{{ item['name'] }}: {{ item['type'] }} (Targets: {{ item['targets'] }})
|
|
||||||
{% if item['kraken'] %}
|
|
||||||
{{ item['name'] }} Top 5 Kraken Results
|
|
||||||
{% for genera in item['kraken'] %}
|
|
||||||
{{ genera['name'] }}: {{ genera['kraken_count'] }} ({{ genera['kraken_percent'] }}){% endfor %}{% endif %}
|
|
||||||
{% endfor %}{% endif %}
|
|
||||||
{% if sub['ext_info'] %}{% for entry in sub['ext_info'] %}
|
|
||||||
Extraction Status:
|
|
||||||
{% for key, value in entry.items() %}
|
|
||||||
{{ key|replace('_', ' ')|title() }}: {{ value }}{% endfor %}{% endfor %}{% endif %}
|
|
||||||
{% if sub['pcr_info'] %}{% for entry in sub['pcr_info'] %}
|
|
||||||
{% if 'comment' not in entry.keys() %}qPCR Momentum Status:{% else %}
|
|
||||||
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 %}
|
|
||||||
Reference in New Issue
Block a user