From e9a1f22cce24f034d03dd94fe0067055449997ad Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Sun, 24 Mar 2024 21:58:38 -0400 Subject: [PATCH 1/4] Fix #828 - allow ensuring parity of bundle.as_pdf() --- docassemble/AssemblyLine/al_document.py | 50 ++++++++++++++++++- .../data/questions/test_bundle_parity.yml | 47 +++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 docassemble/AssemblyLine/data/questions/test_bundle_parity.yml diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index 62cfa684..947d0e0e 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -1,7 +1,7 @@ import re import os import mimetypes -from typing import Any, Dict, List, Union, Callable, Optional +from typing import Any, Dict, List, Literal, Union, Callable, Optional from docassemble.base.util import ( Address, LatitudeLongitude, @@ -34,6 +34,7 @@ from math import floor import subprocess from collections import ChainMap +import pikepdf __all__ = [ "ALAddendumField", @@ -180,6 +181,43 @@ def table_row(title: str, button_htmls: List[str] = []) -> str: return html +def pdf_page_parity(pdf_path:str) -> Literal["even", "odd"]: + """ + Count the number of pages in the PDF and + return "even" if it is divisible by 2 and "odd" + if it is even. + + Args: + pdf_path (str): Path to the PDF in the filesystem + """ + with pikepdf.open(pdf_path) as pdf: + num_pages = len(pdf.pages) + if num_pages % 2 == 0: + return "even" + return "odd" + + +def add_blank_page(pdf_path:str): + # Load the PDF + with pikepdf.open(pdf_path, allow_overwriting_input=True) as pdf: + # Retrieve the last page + last_page = pdf.pages[-1] + + # Extract the size of the last page + media_box = last_page.MediaBox + + # Create a new blank page with the same dimensions as the last page + blank_page = pikepdf.Page(pikepdf.Dictionary( + MediaBox=media_box + )) + + # Add the blank page to the end of the PDF + pdf.pages.append(blank_page) + + # Overwrite the original PDF with the modified version + pdf.save(pdf_path) + + class ALAddendumField(DAObject): """ Represents a field with attributes determining its display in an addendum, typically for PDF templates. @@ -1481,6 +1519,7 @@ def as_pdf( refresh: bool = True, pdfa: bool = False, append_matching_suffix: bool = True, + ensure_parity: Optional[Literal["even", "odd"]] = None, ) -> Optional[DAFile]: """ Returns a consolidated PDF of all enabled documents in the bundle. @@ -1491,6 +1530,8 @@ def as_pdf( pdfa (bool): If True, generates a PDF/A compliant document, defaults to False. append_matching_suffix (bool): Flag to determine if matching suffix should be appended to file name, default is True. Used primarily to enhance automated tests. + ensure_parity (Optional[Literal["even", "odd"]]): Ensures the number of pages in the PDF is even or odd. If omitted, + no parity is enforced. Defaults to None. Returns: Optional[DAFile]: Combined PDF file or None if no documents are enabled. @@ -1533,6 +1574,13 @@ def as_pdf( ) pdf.title = self.title setattr(self.cache, safe_key, pdf) + + if ensure_parity: + if pdf_page_parity(pdf.path()) == ensure_parity: + return pdf + else: + add_blank_page(pdf.path()) + return pdf def __str__(self) -> str: diff --git a/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml b/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml new file mode 100644 index 00000000..ec33ba93 --- /dev/null +++ b/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml @@ -0,0 +1,47 @@ +--- +include: + - assembly_line.yml +--- +objects: + - the_doc: ALDocument.using( + title="The document", + filename="the_document", + enabled=True, + has_addendum=False + ) +--- +objects: + - even_bundle: ALDocumentBundle.using( + title="Even bundle", + filename="the_bundle", + elements=[ + the_doc + ] + ) + - odd_bundle: ALDocumentBundle.using( + title="Even bundle", + filename="the_bundle", + elements=[ + the_doc, + the_doc + ] + ) + +--- +attachment: + variable name: the_doc[i] + content: | + Test content +--- +mandatory: True +question: | + About to make a PDF +continue button field: the_field +--- +mandatory: True +question: | + Download +subquestion: | + ${ even_bundle.as_pdf(ensure_parity="even") } + + ${ odd_bundle.as_pdf(ensure_parity="odd") } \ No newline at end of file From 1d3aa0f5e4c35818410649653a1e7a90be0e5c81 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Sun, 24 Mar 2024 22:28:05 -0400 Subject: [PATCH 2/4] #828, allow default bundle parity --- docassemble/AssemblyLine/al_document.py | 9 ++++- .../data/questions/test_bundle_parity.yml | 38 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index 947d0e0e..2fa38452 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -1479,6 +1479,7 @@ class ALDocumentBundle(DAList): enabled (bool, optional): Determines if the bundle is active. Defaults to True. auto_gather (bool, optional): Automatically gathers attributes. Defaults to False. gathered (bool, optional): Specifies if attributes have been gathered. Defaults to True. + default_parity (Optional[Literal["even", "odd"]]): Default parity to enforce on the PDF. Defaults to None. Examples: Given three documents: `Cover page`, `Main motion form`, and `Notice of Interpreter Request`, @@ -1575,7 +1576,13 @@ def as_pdf( pdf.title = self.title setattr(self.cache, safe_key, pdf) - if ensure_parity: + if hasattr(self, "default_parity") and not ensure_parity: + ensure_parity = self.default_parity + + if ensure_parity not in [None, "even", "odd"]: + raise ValueError("ensure_parity must be either 'even', 'odd' or None") + + if ensure_parity: # Check for odd/even requirement if pdf_page_parity(pdf.path()) == ensure_parity: return pdf else: diff --git a/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml b/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml index ec33ba93..bc30cede 100644 --- a/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml +++ b/docassemble/AssemblyLine/data/questions/test_bundle_parity.yml @@ -16,7 +16,9 @@ objects: filename="the_bundle", elements=[ the_doc - ] + ], + default_parity="even", + enabled=True ) - odd_bundle: ALDocumentBundle.using( title="Even bundle", @@ -24,7 +26,29 @@ objects: elements=[ the_doc, the_doc - ] + ], + enabled=True + ) + - bundle_with_default: ALDocumentBundle.using( + title="Even bundle", + filename="the_bundle", + elements=[ + the_doc + ], + default_parity="even", + enabled=True + ) + +--- +objects: + - bundle_of_bundles: ALDocumentBundle.using( + title="Bundle of bundles", + filename="the_bundle", + elements=[ + even_bundle, + bundle_with_default + ], + enabled=True ) --- @@ -42,6 +66,14 @@ mandatory: True question: | Download subquestion: | + # Should be even ${ even_bundle.as_pdf(ensure_parity="even") } - ${ odd_bundle.as_pdf(ensure_parity="odd") } \ No newline at end of file + # should be odd + ${ odd_bundle.as_pdf(ensure_parity="odd") } + + # should be even + ${ bundle_with_default.as_pdf() } + + # Should be even + ${ bundle_of_bundles.as_pdf() } \ No newline at end of file From 1e3c2e787f17776086504dfb25eaabe1e518ee5c Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Sun, 24 Mar 2024 22:43:25 -0400 Subject: [PATCH 3/4] Black and docsig --- docassemble/AssemblyLine/al_document.py | 35 +++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index 2fa38452..174bd87a 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -181,7 +181,7 @@ def table_row(title: str, button_htmls: List[str] = []) -> str: return html -def pdf_page_parity(pdf_path:str) -> Literal["even", "odd"]: +def pdf_page_parity(pdf_path: str) -> Literal["even", "odd"]: """ Count the number of pages in the PDF and return "even" if it is divisible by 2 and "odd" @@ -189,34 +189,41 @@ def pdf_page_parity(pdf_path:str) -> Literal["even", "odd"]: Args: pdf_path (str): Path to the PDF in the filesystem + + Returns: + Literal["even", "odd"]: The parity of the number of pages in the PDF """ with pikepdf.open(pdf_path) as pdf: num_pages = len(pdf.pages) if num_pages % 2 == 0: return "even" return "odd" - -def add_blank_page(pdf_path:str): + +def add_blank_page(pdf_path: str) -> None: + """ + Add a blank page to the end of a PDF. + + Args: + pdf_path (str): Path to the PDF in the filesystem + """ # Load the PDF with pikepdf.open(pdf_path, allow_overwriting_input=True) as pdf: # Retrieve the last page last_page = pdf.pages[-1] - + # Extract the size of the last page media_box = last_page.MediaBox - + # Create a new blank page with the same dimensions as the last page - blank_page = pikepdf.Page(pikepdf.Dictionary( - MediaBox=media_box - )) - + blank_page = pikepdf.Page(pikepdf.Dictionary(MediaBox=media_box)) + # Add the blank page to the end of the PDF pdf.pages.append(blank_page) - + # Overwrite the original PDF with the modified version pdf.save(pdf_path) - + class ALAddendumField(DAObject): """ @@ -1575,14 +1582,14 @@ def as_pdf( ) pdf.title = self.title setattr(self.cache, safe_key, pdf) - + if hasattr(self, "default_parity") and not ensure_parity: ensure_parity = self.default_parity if ensure_parity not in [None, "even", "odd"]: raise ValueError("ensure_parity must be either 'even', 'odd' or None") - - if ensure_parity: # Check for odd/even requirement + + if ensure_parity: # Check for odd/even requirement if pdf_page_parity(pdf.path()) == ensure_parity: return pdf else: From 01f83e33e0a018423bd30e35c65d3ef8d7797685 Mon Sep 17 00:00:00 2001 From: Quinten Steenhuis Date: Mon, 25 Mar 2024 20:26:04 -0400 Subject: [PATCH 4/4] Update docassemble/AssemblyLine/al_document.py --- docassemble/AssemblyLine/al_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index 174bd87a..3521c3a7 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -185,7 +185,7 @@ def pdf_page_parity(pdf_path: str) -> Literal["even", "odd"]: """ Count the number of pages in the PDF and return "even" if it is divisible by 2 and "odd" - if it is even. + if it is not divisible by 2. Args: pdf_path (str): Path to the PDF in the filesystem