From acab9d0f4c32b1e586b38afbb372e1f73bc68e2d Mon Sep 17 00:00:00 2001 From: lwark Date: Tue, 1 Oct 2024 13:33:01 -0500 Subject: [PATCH] Returned to pdf exports from details. --- CHANGELOG.md | 1 + .../backend/db/models/submissions.py | 21 +++++------ src/submissions/backend/excel/writer.py | 5 +-- src/submissions/backend/validators/pydant.py | 17 +++++++-- .../frontend/widgets/submission_details.py | 33 ++++++++---------- .../templates/wastewater_subdocument.docx | Bin 12900 -> 12761 bytes 6 files changed, 43 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ccd40..e093436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 202409.05 +- Replaced some lists with generators to improve speed, added javascript to templates for click events. - Added in custom field for BasicSubmission which will allow limited new fields to be added to generic submission types. ## 202409.04 diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 073165c..0048192 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1042,7 +1042,7 @@ class BasicSubmission(BaseClass): # return [item.to_sub_dict() for item in self.submission_sample_associations] @classmethod - def get_details_template(cls, base_dict: dict) -> Tuple[dict, Template]: + def get_details_template(cls, base_dict: dict) -> Template: """ Get the details jinja template for the correct class @@ -1357,13 +1357,13 @@ class BasicSubmission(BaseClass): if fname.name == "": # logger.debug(f"export cancelled.") return - if full_backup: - backup = self.to_dict(full_data=True) - try: - with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f: - yaml.dump(backup, f) - except KeyError as e: - logger.error(f"Problem saving yml backup file: {e}") + # if full_backup: + # backup = self.to_dict(full_data=True) + # try: + # with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f: + # yaml.dump(backup, f) + # except KeyError as e: + # logger.error(f"Problem saving yml backup file: {e}") writer = pyd.to_writer() writer.xl.save(filename=fname.with_suffix(".xlsx")) @@ -1632,6 +1632,8 @@ class Wastewater(BasicSubmission): dict: Updated information """ input_dict = super().finalize_details(input_dict) + # NOTE: Currently this is preserving the generator items, can we come up with a better way? + input_dict['samples'] = [sample for sample in input_dict['samples']] dummy_samples = [] for item in input_dict['samples']: # logger.debug(f"Sample dict: {item}") @@ -1681,12 +1683,10 @@ class Wastewater(BasicSubmission): for sample in self.samples: # logger.debug(f"Running update on: {sample}") try: - # sample_dict = [item for item in parser.samples if item['sample'] == sample.rsl_number][0] sample_dict = next(item for item in parser.samples if item['sample'] == sample.rsl_number) except StopIteration: continue 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: dict, tpl_obj=None) -> dict: @@ -1703,6 +1703,7 @@ class Wastewater(BasicSubmission): from backend.excel.writer import DocxWriter input_dict = super().custom_docx_writer(input_dict) well_24 = [] + input_dict['samples'] = [item for item in input_dict['samples']] samples_copy = deepcopy(input_dict['samples']) for sample in sorted(samples_copy, key=itemgetter('column', 'row')): try: diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 79e5296..61f8104 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -519,6 +519,7 @@ class DocxWriter(object): Args: base_dict (dict): dictionary of info to be written to template. """ + logger.debug(f"Incoming base dict: {pformat(base_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()}_subdocument.docx" @@ -528,8 +529,8 @@ class DocxWriter(object): 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'])) + base_dict['platemap'] = [item for item in self.create_plate_map(base_dict['samples'], rows=8, columns=12)] + # logger.debug(pformat(base_dict['platemap'])) try: base_dict['excluded'] += ["platemap"] except KeyError: diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 82e29ac..5942616 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -739,11 +739,22 @@ class PydSubmission(BaseModel, extra='allow'): pass else: # logger.debug("Extracting 'value' from attributes") - output = {k: (getattr(self, k) if not isinstance(getattr(self, k), dict) else getattr(self, k)['value']) for - k in fields} - + output = {k: self.filter_field(k) for k in fields} return output + def filter_field(self, key:str): + item = getattr(self, key) + # logger.debug(f"Attempting deconstruction of {key}: {item} with type {type(item)}") + match item: + case dict(): + try: + item = item['value'] + except KeyError: + logger.error(f"Couldn't get dict value: {item}") + case _: + pass + return item + def find_missing(self) -> Tuple[dict, dict]: """ Retrieves info and reagents marked as missing. diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 30e2a51..ab15294 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -1,12 +1,13 @@ """ Webview to show submission and sample details. """ -from PyQt6.QtGui import QColor +from PyQt6.QtGui import QColor, QPageSize, QPageLayout +from PyQt6.QtPrintSupport import QPrinter from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QDialogButtonBox, QTextEdit, QGridLayout) from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebChannel import QWebChannel -from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF from jinja2 import TemplateNotFound from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType @@ -43,7 +44,7 @@ class SubmissionDetails(QDialog): self.layout = QGridLayout() # self.setFixedSize(900, 500) # NOTE: button to export a pdf version - self.btn = QPushButton("Export DOCX") + self.btn = QPushButton("Export PDF") self.btn.setFixedWidth(775) self.btn.clicked.connect(self.export) self.back = QPushButton("Back") @@ -151,7 +152,8 @@ class SubmissionDetails(QDialog): self.base_dict = submission.finalize_details(self.base_dict) # 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['platemap'] = submission.make_plate_map(sample_list=submission.hitpick_plate()) + self.base_dict['excluded'] = submission.get_default_info("details_ignore") 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: @@ -159,11 +161,10 @@ class SubmissionDetails(QDialog): # logger.debug(f"Submission_details: {pformat(self.base_dict)}") # logger.debug(f"User is power user: {is_power_user()}") self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css) - with open("test.html", "w") as f: - f.write(self.html) + # with open("test.html", "w") as f: + # f.write(self.html) self.webview.setHtml(self.html) - @pyqtSlot(str) def sign_off(self, submission: str | BasicSubmission): # logger.debug(f"Signing off on {submission} - ({getuser()})") @@ -177,18 +178,12 @@ class SubmissionDetails(QDialog): """ Renders submission to html, then creates and saves .pdf file to user selected file. """ - export_plate = BasicSubmission.query(rsl_plate_num=self.export_plate) - base_dict = export_plate.to_dict(full_data=True) - base_dict['excluded'] = export_plate.get_default_info('details_ignore') - logger.debug(f"base dict: {pformat(base_dict)}") - writer = DocxWriter(base_dict=base_dict) - fname = select_save_file(obj=self, default_name=base_dict['plate_number'], extension="docx") - writer.save(fname) - # try: - # html_to_pdf(html=self.html, output_file=fname) - # except PermissionError as e: - # logger.error(f"Error saving pdf: {e}") - + fname = select_save_file(obj=self, default_name=self.export_plate, extension="pdf") + page_layout = QPageLayout() + page_layout.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) + page_layout.setOrientation(QPageLayout.Orientation.Portrait) + page_layout.setMargins(QMarginsF(25, 25, 25, 25)) + self.webview.page().printToPdf(fname.with_suffix(".pdf").__str__(), page_layout) class SubmissionComment(QDialog): """ diff --git a/src/submissions/templates/wastewater_subdocument.docx b/src/submissions/templates/wastewater_subdocument.docx index 0d6d18625650460100a6f0e6d2a967cd03005e95..17c21c42ecb0a4a335db74159968a60e5ff01acb 100644 GIT binary patch delta 3484 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 z000000001q9FxE?A{?S?arSos008X*000pH000000000000000qaTz0F+BlMlRPpa K1_Cbt0000jEOI#j delta 3655 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 GqyGW$x7ZH=