diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 3419f28..6a0d6f1 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -4,6 +4,7 @@ Models for the main submission and sample types. from __future__ import annotations import sys +from copy import deepcopy from getpass import getuser import logging, uuid, tempfile, re, yaml, base64 from zipfile import ZipFile @@ -728,6 +729,11 @@ class BasicSubmission(BaseClass): logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} autofill") return input_excel + @classmethod + def custom_docx_writer(cls, input_dict): + + return input_dict + @classmethod def enforce_name(cls, instr: str, data: dict | None = {}) -> str: """ @@ -1439,7 +1445,7 @@ class Wastewater(BasicSubmission): dummy_samples = [] for item in input_dict['samples']: # logger.debug(f"Sample dict: {item}") - thing = item + thing = deepcopy(item) try: thing['row'] = thing['source_row'] thing['column'] = thing['source_column'] @@ -1486,6 +1492,28 @@ class Wastewater(BasicSubmission): self.update_subsampassoc(sample=sample, input_dict=sample_dict) # self.report.add_result(Result(msg=f"We added PCR info to {sub.rsl_plate_num}.", status='Information')) + @classmethod + def custom_docx_writer(cls, input_dict): + from backend.excel.writer import DocxWriter + input_dict = super().custom_docx_writer(input_dict) + well_24 = [] + samples_copy = deepcopy(input_dict['samples']) + for sample in sorted(samples_copy, key=itemgetter('column', 'row')): + # for sample in input_dict['samples']: + try: + row = sample['source_row'] + except KeyError: + continue + try: + column = sample['source_column'] + except KeyError: + continue + copy = dict(submitter_id=sample['submitter_id'], row=row, column=column) + well_24.append(copy) + input_dict['origin_plate'] = DocxWriter.create_plate_map(sample_list=well_24, rows=4, columns=6) + return input_dict + + class WastewaterArtic(BasicSubmission): """ diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 8bbe4ec..d65ad40 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -1,10 +1,11 @@ import logging from copy import copy +from operator import itemgetter from pathlib import Path # from pathlib import Path from pprint import pformat from typing import List - +from collections import OrderedDict from jinja2 import TemplateNotFound from openpyxl import load_workbook, Workbook from backend.db.models import SubmissionType, KitType, BasicSubmission @@ -13,6 +14,7 @@ from io import BytesIO from collections import OrderedDict from tools import jinja_template_loading from docxtpl import DocxTemplate +from docx import Document logger = logging.getLogger(f"submissions.{__name__}") @@ -460,14 +462,65 @@ class TipWriter(object): class DocxWriter(object): def __init__(self, base_dict: dict): + self.sub_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=base_dict['submission_type']) env = jinja_template_loading() - temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_document.docx" - path = Path(env.loader.__getattribute__("searchpath")[0]).joinpath(temp_name) - template = DocxTemplate(path) + temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx" + path = Path(env.loader.__getattribute__("searchpath")[0]) + main_template = path.joinpath("basicsubmission_document.docx") + subdocument = path.joinpath(temp_name) + if subdocument.exists(): + main_template = self.create_merged_template(main_template, subdocument) + self.template = DocxTemplate(main_template) + base_dict['platemap'] = self.create_plate_map(base_dict['samples'], rows=8, columns=12) + # logger.debug(pformat(base_dict['plate_map'])) try: - template.render(base_dict) - except FileNotFoundError: - template = DocxTemplate( - Path(env.loader.__getattribute__("searchpath")[0]).joinpath("basicsubmission_document.docx")) - template.render({"sub": base_dict}) - template.save("test.docx") + base_dict['excluded'] += ["platemap"] + except KeyError: + base_dict['excluded'] = ["platemap"] + base_dict = self.sub_obj.custom_docx_writer(base_dict) + # logger.debug(f"Base dict: {pformat(base_dict)}") + self.template.render({"sub": base_dict}) + + @classmethod + def create_plate_map(self, sample_list: List[dict], rows: int = 0, columns: int = 0) -> List[list]: + sample_list = sorted(sample_list, key=itemgetter('column', 'row')) + if rows == 0: + rows = max([sample['row'] for sample in sample_list]) + if columns == 0: + columns = max([sample['column'] for sample in sample_list]) + output = [] + for row in range(0, rows): + contents = [''] * columns + for column in range(0, columns): + try: + ooi = [item for item in sample_list if item['row']==row+1 and item['column']==column+1][0] + except IndexError: + continue + contents[column] = ooi['submitter_id'] + # contents = [sample['submitter_id'] for sample in sample_list if sample['row'] == row + 1] + # contents = [f"{sample['row']},{sample['column']}" for sample in sample_list if sample['row'] == row + 1] + if len(contents) < columns: + contents += [''] * (columns - len(contents)) + if not contents: + contents = [''] * columns + output.append(contents) + return output + + def create_merged_template(self, *args): + merged_document = Document() + output = BytesIO() + for index, file in enumerate(args): + sub_doc = Document(file) + # Don't add a page break if you've reached the last file. + # if index < len(args) - 1: + # sub_doc.add_page_break() + for element in sub_doc.element.body: + merged_document.element.body.append(element) + merged_document.save(output) + return output + + + def save(self, filename: Path | str): + if isinstance(filename, str): + filename = Path(filename) + self.template.save(filename) diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 2ef4951..2a30cce 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -327,17 +327,17 @@ class PydEquipment(BaseModel, extra='ignore'): process = Process.query(name=self.processes[0]) if process is None: logger.error(f"Found unknown process: {process}.") - from frontend.widgets.pop_ups import QuestionAsker - dlg = QuestionAsker(title="Add Process?", - message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?") - if dlg.exec(): - kit = submission.extraction_kit - submission_type = submission.submission_type - process = Process(name=self.processes[0]) - process.kit_types.append(kit) - process.submission_types.append(submission_type) - process.equipment.append(equipment) - process.save() + # from frontend.widgets.pop_ups import QuestionAsker + # dlg = QuestionAsker(title="Add Process?", + # message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?") + # if dlg.exec(): + # kit = submission.extraction_kit + # submission_type = submission.submission_type + # process = Process(name=self.processes[0]) + # process.kit_types.append(kit) + # process.submission_types.append(submission_type) + # process.equipment.append(equipment) + # process.save() assoc.process = process assoc.role = self.role else: @@ -727,7 +727,7 @@ class PydSubmission(BaseModel, extra='allow'): if equip is None: continue equip, association = equip.toSQL(submission=instance) - if association is not None: + if association is not None and association not in instance.submission_equipment_associations: # association.save() # logger.debug( # f"Equipment association SQL object to be added to submission: {association.__dict__}") @@ -738,7 +738,7 @@ class PydSubmission(BaseModel, extra='allow'): continue logger.debug(f"Converting tips: {tips} to sql.") association = tips.to_sql(submission=instance) - if association is not None: + if association is not None and association not in instance.submission_tips_associations: # association.save() instance.submission_tips_associations.append(association) case item if item in instance.jsons(): diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index 3570cf2..8af2167 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -59,7 +59,7 @@ class EquipmentUsage(QDialog): case _: pass logger.debug(f"parsed output of Equsage form: {pformat(output)}") - return [item for item in output if item is not None] + return [item.strip() for item in output if item is not None] class LabelRow(QWidget): @@ -165,7 +165,7 @@ class RoleComboBox(QWidget): try: return PydEquipment( name=eq.name, - processes=[self.process.currentText()], + processes=[self.process.currentText().strip()], role=self.role.name, asset_number=eq.asset_number, nickname=eq.nickname, diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index b313810..911d76f 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -1,3 +1,5 @@ +from PyQt6.QtGui import QPageSize +from PyQt6.QtPrintSupport import QPrinter from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox, QDialogButtonBox, QTextEdit) from PyQt6.QtWebEngineWidgets import QWebEngineView @@ -17,6 +19,8 @@ from pprint import pformat from html2image import Html2Image from PIL import Image from typing import List +from backend.excel.writer import DocxWriter + logger = logging.getLogger(f"submissions.{__name__}") @@ -90,13 +94,17 @@ class SubmissionDetails(QDialog): # del self.base_dict['id'] # logger.debug(f"Creating barcode.") # logger.debug(f"Making platemap...") + self.base_dict['platemap'] = BasicSubmission.make_plate_map(sample_list=submission.hitpick_plate()) self.base_dict, self.template = submission.get_details_template(base_dict=self.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() logger.debug(f"Submission_details: {pformat(self.base_dict)}") - self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user()) + self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css) self.webview.setHtml(self.html) - # with open("test.html", "w") as f: - # f.write(self.html) + with open("test.html", "w") as f: + f.write(self.html) self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}") @pyqtSlot(str) @@ -112,31 +120,32 @@ class SubmissionDetails(QDialog): """ Renders submission to html, then creates and saves .pdf file to user selected file. """ - logger.debug(f"Base dict: {pformat(self.base_dict)}") + writer = DocxWriter(base_dict=self.base_dict) fname = select_save_file(obj=self, default_name=self.base_dict['plate_number'], extension="docx") - image_io = BytesIO() - temp_dir = Path(TemporaryDirectory().name) - hti = Html2Image(output_path=temp_dir, size=(2400, 1500)) - temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name) - screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name) - export_map = Image.open(screenshot[0]) - export_map = export_map.convert('RGB') + writer.save(fname) + # image_io = BytesIO() + # temp_dir = Path(TemporaryDirectory().name) + # hti = Html2Image(output_path=temp_dir, size=(2400, 1500)) + # temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name) + # screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name) + # export_map = Image.open(screenshot[0]) + # export_map = export_map.convert('RGB') + # try: + # export_map.save(image_io, 'JPEG') + # except AttributeError: + # logger.error(f"No plate map found") + # self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8') + # del self.base_dict['platemap'] + # self.html2 = self.template.render(sub=self.base_dict) try: - export_map.save(image_io, 'JPEG') - except AttributeError: - logger.error(f"No plate map found") - self.base_dict['export_map'] = base64.b64encode(image_io.getvalue()).decode('utf-8') - del self.base_dict['platemap'] - self.html2 = self.template.render(sub=self.base_dict) - try: - html_to_pdf(html=self.html2, output_file=fname) + html_to_pdf(html=self.html, output_file=fname) except PermissionError as e: logger.error(f"Error saving pdf: {e}") - msg = QMessageBox() - msg.setText("Permission Error") - msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.") - msg.setWindowTitle("Permission Error") - msg.exec() + # msg = QMessageBox() + # msg.setText("Permission Error") + # msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.") + # msg.setWindowTitle("Permission Error") + # msg.exec() class SubmissionComment(QDialog): diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 9055571..d66433d 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -312,6 +312,7 @@ class SubmissionFormWidget(QWidget): if dlg.exec(): # NOTE: Do not add duplicate reagents. result = None + else: self.app.ctx.database_session.rollback() report.add_result(Result(msg="Overwrite cancelled", status="Information")) diff --git a/src/submissions/templates/bacterialculture_subdocument.docx b/src/submissions/templates/bacterialculture_subdocument.docx new file mode 100644 index 0000000..03e7ca2 Binary files /dev/null and b/src/submissions/templates/bacterialculture_subdocument.docx differ diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicsubmission_details.html index 23c31e7..8dbf00c 100644 --- a/src/submissions/templates/basicsubmission_details.html +++ b/src/submissions/templates/basicsubmission_details.html @@ -2,38 +2,11 @@ {% block head %} + {% if css %} + {% endif %} Submission Details for {{ sub['Plate Number'] }} {% endblock %} diff --git a/src/submissions/templates/basicsubmission_document.docx b/src/submissions/templates/basicsubmission_document.docx new file mode 100644 index 0000000..5e9bd49 Binary files /dev/null and b/src/submissions/templates/basicsubmission_document.docx differ diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css new file mode 100644 index 0000000..b19cf61 --- /dev/null +++ b/src/submissions/templates/css/styles.css @@ -0,0 +1,29 @@ +/* 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; +} diff --git a/src/submissions/templates/wastewater_subdocument.docx b/src/submissions/templates/wastewater_subdocument.docx new file mode 100644 index 0000000..cdbfa21 Binary files /dev/null and b/src/submissions/templates/wastewater_subdocument.docx differ diff --git a/src/submissions/tools.py b/src/submissions/tools.py index aec3c4c..05aeb03 100644 --- a/src/submissions/tools.py +++ b/src/submissions/tools.py @@ -658,11 +658,13 @@ def html_to_pdf(html, output_file: Path | str): logger.debug(f"Printing PDF to {output_file}") document = QWebEngineView() document.setHtml(html) + # document.show() printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setOutputFileName(output_file.absolute().__str__()) printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) document.print(printer) + # document.close() # HTML(string=html).write_pdf(output_file) # new_parser = HtmlToDocx() # docx = new_parser.parse_html_string(html)