New docx templates

This commit is contained in:
lwark
2024-06-20 13:31:36 -05:00
parent 337112a27d
commit 384bd567ae
12 changed files with 175 additions and 80 deletions

View File

@@ -4,6 +4,7 @@ Models for the main submission and sample types.
from __future__ import annotations from __future__ import annotations
import sys import sys
from copy import deepcopy
from getpass import getuser from getpass import getuser
import logging, uuid, tempfile, re, yaml, base64 import logging, uuid, tempfile, re, yaml, base64
from zipfile import ZipFile from zipfile import ZipFile
@@ -728,6 +729,11 @@ class BasicSubmission(BaseClass):
logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} autofill") logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} autofill")
return input_excel return input_excel
@classmethod
def custom_docx_writer(cls, input_dict):
return input_dict
@classmethod @classmethod
def enforce_name(cls, instr: str, data: dict | None = {}) -> str: def enforce_name(cls, instr: str, data: dict | None = {}) -> str:
""" """
@@ -1439,7 +1445,7 @@ class Wastewater(BasicSubmission):
dummy_samples = [] dummy_samples = []
for item in input_dict['samples']: for item in input_dict['samples']:
# logger.debug(f"Sample dict: {item}") # logger.debug(f"Sample dict: {item}")
thing = item thing = deepcopy(item)
try: try:
thing['row'] = thing['source_row'] thing['row'] = thing['source_row']
thing['column'] = thing['source_column'] thing['column'] = thing['source_column']
@@ -1486,6 +1492,28 @@ class Wastewater(BasicSubmission):
self.update_subsampassoc(sample=sample, input_dict=sample_dict) 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')) # 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): class WastewaterArtic(BasicSubmission):
""" """

View File

@@ -1,10 +1,11 @@
import logging import logging
from copy import copy from copy import copy
from operator import itemgetter
from pathlib import Path from pathlib import Path
# from pathlib import Path # from pathlib import Path
from pprint import pformat from pprint import pformat
from typing import List from typing import List
from collections import OrderedDict
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
from openpyxl import load_workbook, Workbook from openpyxl import load_workbook, Workbook
from backend.db.models import SubmissionType, KitType, BasicSubmission from backend.db.models import SubmissionType, KitType, BasicSubmission
@@ -13,6 +14,7 @@ from io import BytesIO
from collections import OrderedDict from collections import OrderedDict
from tools import jinja_template_loading from tools import jinja_template_loading
from docxtpl import DocxTemplate from docxtpl import DocxTemplate
from docx import Document
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -460,14 +462,65 @@ class TipWriter(object):
class DocxWriter(object): class DocxWriter(object):
def __init__(self, base_dict: dict): def __init__(self, base_dict: dict):
self.sub_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=base_dict['submission_type'])
env = jinja_template_loading() env = jinja_template_loading()
temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_document.docx" temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx"
path = Path(env.loader.__getattribute__("searchpath")[0]).joinpath(temp_name) path = Path(env.loader.__getattribute__("searchpath")[0])
template = DocxTemplate(path) 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: try:
template.render(base_dict) base_dict['excluded'] += ["platemap"]
except FileNotFoundError: except KeyError:
template = DocxTemplate( base_dict['excluded'] = ["platemap"]
Path(env.loader.__getattribute__("searchpath")[0]).joinpath("basicsubmission_document.docx")) base_dict = self.sub_obj.custom_docx_writer(base_dict)
template.render({"sub": base_dict}) # logger.debug(f"Base dict: {pformat(base_dict)}")
template.save("test.docx") 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)

View File

@@ -327,17 +327,17 @@ class PydEquipment(BaseModel, extra='ignore'):
process = Process.query(name=self.processes[0]) process = Process.query(name=self.processes[0])
if process is None: if process is None:
logger.error(f"Found unknown process: {process}.") logger.error(f"Found unknown process: {process}.")
from frontend.widgets.pop_ups import QuestionAsker # from frontend.widgets.pop_ups import QuestionAsker
dlg = QuestionAsker(title="Add Process?", # dlg = QuestionAsker(title="Add Process?",
message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?") # message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
if dlg.exec(): # if dlg.exec():
kit = submission.extraction_kit # kit = submission.extraction_kit
submission_type = submission.submission_type # submission_type = submission.submission_type
process = Process(name=self.processes[0]) # process = Process(name=self.processes[0])
process.kit_types.append(kit) # process.kit_types.append(kit)
process.submission_types.append(submission_type) # process.submission_types.append(submission_type)
process.equipment.append(equipment) # process.equipment.append(equipment)
process.save() # process.save()
assoc.process = process assoc.process = process
assoc.role = self.role assoc.role = self.role
else: else:
@@ -727,7 +727,7 @@ class PydSubmission(BaseModel, extra='allow'):
if equip is None: if equip is None:
continue continue
equip, association = equip.toSQL(submission=instance) 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() # association.save()
# logger.debug( # logger.debug(
# f"Equipment association SQL object to be added to submission: {association.__dict__}") # f"Equipment association SQL object to be added to submission: {association.__dict__}")
@@ -738,7 +738,7 @@ class PydSubmission(BaseModel, extra='allow'):
continue continue
logger.debug(f"Converting tips: {tips} to sql.") logger.debug(f"Converting tips: {tips} to sql.")
association = tips.to_sql(submission=instance) 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() # association.save()
instance.submission_tips_associations.append(association) instance.submission_tips_associations.append(association)
case item if item in instance.jsons(): case item if item in instance.jsons():

View File

@@ -59,7 +59,7 @@ class EquipmentUsage(QDialog):
case _: case _:
pass pass
logger.debug(f"parsed output of Equsage form: {pformat(output)}") 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): class LabelRow(QWidget):
@@ -165,7 +165,7 @@ class RoleComboBox(QWidget):
try: try:
return PydEquipment( return PydEquipment(
name=eq.name, name=eq.name,
processes=[self.process.currentText()], processes=[self.process.currentText().strip()],
role=self.role.name, role=self.role.name,
asset_number=eq.asset_number, asset_number=eq.asset_number,
nickname=eq.nickname, nickname=eq.nickname,

View File

@@ -1,3 +1,5 @@
from PyQt6.QtGui import QPageSize
from PyQt6.QtPrintSupport import QPrinter
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox, from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox,
QDialogButtonBox, QTextEdit) QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
@@ -17,6 +19,8 @@ from pprint import pformat
from html2image import Html2Image from html2image import Html2Image
from PIL import Image from PIL import Image
from typing import List from typing import List
from backend.excel.writer import DocxWriter
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -90,13 +94,17 @@ class SubmissionDetails(QDialog):
# del self.base_dict['id'] # del self.base_dict['id']
# logger.debug(f"Creating barcode.") # logger.debug(f"Creating barcode.")
# logger.debug(f"Making platemap...") # logger.debug(f"Making platemap...")
self.base_dict['platemap'] = BasicSubmission.make_plate_map(sample_list=submission.hitpick_plate()) 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) 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)}") 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) self.webview.setHtml(self.html)
# with open("test.html", "w") as f: with open("test.html", "w") as f:
# f.write(self.html) f.write(self.html)
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}") self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
@pyqtSlot(str) @pyqtSlot(str)
@@ -112,31 +120,32 @@ 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.
""" """
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") fname = select_save_file(obj=self, default_name=self.base_dict['plate_number'], extension="docx")
image_io = BytesIO() writer.save(fname)
temp_dir = Path(TemporaryDirectory().name) # image_io = BytesIO()
hti = Html2Image(output_path=temp_dir, size=(2400, 1500)) # temp_dir = Path(TemporaryDirectory().name)
temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name) # hti = Html2Image(output_path=temp_dir, size=(2400, 1500))
screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name) # temp_file = Path(TemporaryFile(dir=temp_dir, suffix=".png").name)
export_map = Image.open(screenshot[0]) # screenshot = hti.screenshot(self.base_dict['platemap'], save_as=temp_file.name)
export_map = export_map.convert('RGB') # 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: try:
export_map.save(image_io, 'JPEG') html_to_pdf(html=self.html, output_file=fname)
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)
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()
msg.setText("Permission Error") # msg.setText("Permission Error")
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.") # msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error") # msg.setWindowTitle("Permission Error")
msg.exec() # msg.exec()
class SubmissionComment(QDialog): class SubmissionComment(QDialog):

View File

@@ -312,6 +312,7 @@ class SubmissionFormWidget(QWidget):
if dlg.exec(): if dlg.exec():
# NOTE: Do not add duplicate reagents. # NOTE: Do not add duplicate reagents.
result = None result = None
else: else:
self.app.ctx.database_session.rollback() self.app.ctx.database_session.rollback()
report.add_result(Result(msg="Overwrite cancelled", status="Information")) report.add_result(Result(msg="Overwrite cancelled", status="Information"))

View File

@@ -2,38 +2,11 @@
<html> <html>
<head> <head>
{% block head %} {% block head %}
{% if css %}
<style> <style>
/* Tooltip container */ {{ css }}
.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> </style>
{% endif %}
<title>Submission Details for {{ sub['Plate Number'] }}</title> <title>Submission Details for {{ sub['Plate Number'] }}</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script> <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
{% endblock %} {% endblock %}

View File

@@ -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;
}

Binary file not shown.

View File

@@ -658,11 +658,13 @@ def html_to_pdf(html, output_file: Path | str):
logger.debug(f"Printing PDF to {output_file}") logger.debug(f"Printing PDF to {output_file}")
document = QWebEngineView() document = QWebEngineView()
document.setHtml(html) document.setHtml(html)
# document.show()
printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer = QPrinter(QPrinter.PrinterMode.HighResolution)
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
printer.setOutputFileName(output_file.absolute().__str__()) printer.setOutputFileName(output_file.absolute().__str__())
printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4))
document.print(printer) document.print(printer)
# document.close()
# HTML(string=html).write_pdf(output_file) # HTML(string=html).write_pdf(output_file)
# new_parser = HtmlToDocx() # new_parser = HtmlToDocx()
# docx = new_parser.parse_html_string(html) # docx = new_parser.parse_html_string(html)