diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c373d6..66b152b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 202307.04 + +- Individual plate details now in html format. + ## 202307.03 - Auto-filling of some empty cells in excel file. diff --git a/README.md b/README.md index 62b7c15..28287b2 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,16 @@ ## Logging in New Run: *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) - a. All fields should be filled in to ensure proper lookups of reagents. -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. +1. Ensure a properly formatted Submission Excel form has been filled out. + 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. 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. 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. + 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. -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. 10. In case of any mistakes, the run can be overwritten by a reimport. diff --git a/TODO.md b/TODO.md index 22398ad..f79239d 100644 --- a/TODO.md +++ b/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] Code clean-up and refactor (2023-07). - [ ] Migrate context settings to pydantic-settings model. diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index b188763..31cffb3 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -4,7 +4,7 @@ from pathlib import Path # Version of the realpython-reader package __project__ = "submissions" -__version__ = "202307.3b" +__version__ = "202307.4b" __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __copyright__ = "2022-2023, Government of Canada" diff --git a/src/submissions/frontend/custom_widgets/sub_details.py b/src/submissions/frontend/custom_widgets/sub_details.py index f1162b2..1a4d337 100644 --- a/src/submissions/frontend/custom_widgets/sub_details.py +++ b/src/submissions/frontend/custom_widgets/sub_details.py @@ -11,8 +11,9 @@ from PyQt6.QtWidgets import ( QMessageBox, QFileDialog, QMenu, QLabel, QDialogButtonBox, QToolBar ) +from PyQt6.QtWebEngineWidgets import QWebEngineView 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.excel import make_hitpicks from configure import jinja_template_loading @@ -266,40 +267,25 @@ class SubmissionDetails(QDialog): # don't want id del self.base_dict['id'] # retrieve jinja template - template = env.get_template("submission_details.txt") + # template = env.get_template("submission_details.txt") # render using object dict - text = template.render(sub=self.base_dict) + # text = template.render(sub=self.base_dict) # create text field - txt_editor = QTextEdit(self) - txt_editor.setReadOnly(True) - txt_editor.document().setPlainText(text) + # txt_editor = QTextEdit(self) + # txt_editor.setReadOnly(True) + # txt_editor.document().setPlainText(text) # resize - font = txt_editor.document().defaultFont() - fontMetrics = QFontMetrics(font) - textSize = fontMetrics.size(0, txt_editor.toPlainText()) - w = textSize.width() + 10 - h = textSize.height() + 10 - txt_editor.setMinimumSize(w, h) - txt_editor.setMaximumSize(w, h) - txt_editor.resize(w, h) - interior.resize(w,900) - txt_editor.setText(text) - 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 + # font = txt_editor.document().defaultFont() + # fontMetrics = QFontMetrics(font) + # textSize = fontMetrics.size(0, txt_editor.toPlainText()) + # w = textSize.width() + 10 + # h = textSize.height() + 10 + # txt_editor.setMinimumSize(w, h) + # txt_editor.setMaximumSize(w, h) + # txt_editor.resize(w, h) + # interior.resize(w,900) + # txt_editor.setText(text) + # interior.setWidget(txt_editor) 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) @@ -312,10 +298,45 @@ class SubmissionDetails(QDialog): 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) + template = env.get_template("submission_details.html") + self.html = template.render(sub=self.base_dict) + webview = QWebEngineView() + 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: home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__() except FileNotFoundError: @@ -326,7 +347,7 @@ class SubmissionDetails(QDialog): return try: with open(fname, "w+b") as f: - pisa.CreatePDF(html, dest=f) + pisa.CreatePDF(self.html, dest=f) except PermissionError as e: logger.error(f"Error saving pdf: {e}") msg = QMessageBox() diff --git a/src/submissions/frontend/main_window_functions.py b/src/submissions/frontend/main_window_functions.py index e1b379a..3f41216 100644 --- a/src/submissions/frontend/main_window_functions.py +++ b/src/submissions/frontend/main_window_functions.py @@ -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): """ - Automatically fills in excel cells with reagent info. + Automatically fills in excel cells with submission info. Args: 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 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} - # 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] + # hacky manipulation of submission type so it looks better. 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} 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: new_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']['value'] = reagent['expiry'] new_reagent['sheet'] = relevant_map[new_reagent['type']]['sheet'] + # name is only present for Bacterial Culture try: new_reagent['name'] = relevant_map[new_reagent['type']]['name'] new_reagent['name']['value'] = reagent['type'] except: pass new_reagents.append(new_reagent) + # construct new info objects to put into excel sheets new_info = [] for item in relevant_info: 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_info.append(new_item) logger.debug(f"New reagents: {new_reagents}") + # open the workbook using openpyxl workbook = load_workbook(obj.xl) + # get list of sheet names sheets = workbook.sheetnames - logger.debug(workbook.sheetnames) + # logger.debug(workbook.sheetnames) for sheet in sheets: + # open sheet worksheet=workbook[sheet] + # Get relevant reagents for that sheet sheet_reagents = [item for item in new_reagents if sheet in item['sheet']] for reagent in sheet_reagents: 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()) except: pass + # Get relevant info for that sheet sheet_info = [item for item in new_info if sheet in item['location']['sheets']] for item in sheet_info: logger.debug(f"Attempting: {item['type']}") worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value']) + # Hacky way to if info['submission_type'] == "Bacterial Culture": workbook["Sample List"].cell(row=14, column=2, value=getuser()) fname = select_save_file(obj=obj, default_name=info['rsl_plate_num'], extension="xlsx") diff --git a/src/submissions/templates/submission_details.txt b/src/submissions/templates/submission_details.txt deleted file mode 100644 index d9dcfc4..0000000 --- a/src/submissions/templates/submission_details.txt +++ /dev/null @@ -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 %} \ No newline at end of file