diff --git a/docassemble/AssemblyLine/al_document.py b/docassemble/AssemblyLine/al_document.py index 0ba5159e..da12f508 100644 --- a/docassemble/AssemblyLine/al_document.py +++ b/docassemble/AssemblyLine/al_document.py @@ -2277,6 +2277,7 @@ def as_pdf( pdfa: bool = False, append_matching_suffix: bool = True, ensure_parity: Optional[Literal["even", "odd"]] = None, + use_previously_cached_files: bool = False, ) -> Optional[DAFile]: """ Returns a consolidated PDF of all enabled documents in the bundle. @@ -2289,6 +2290,10 @@ def as_pdf( 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. + use_previously_cached_files (bool): Reuse a final bundled PDF previously + assembled in the background, if available. Defaults to False. Custom + keys, PDF/A, and page parity requests use the normal assembly path, + because the background cache does not identify those options. Returns: Optional[DAFile]: Combined PDF file or None if no documents are enabled. @@ -2306,6 +2311,18 @@ def as_pdf( ${ al_user_bundle.as_pdf(key="preview") } ``` """ + if ( + use_previously_cached_files + and key == "final" + and not pdfa + and ensure_parity is None + and not getattr(self, "default_parity", None) + and hasattr(self, "_downloadable_files") + ): + _, _, bundled_pdf = self._downloadable_files + if bundled_pdf is not None: + return bundled_pdf + safe_key = space_to_underscore(key) if pdfa: safe_key = safe_key + "-pdfa" diff --git a/docassemble/AssemblyLine/test_al_document.py b/docassemble/AssemblyLine/test_al_document.py index c93a7890..bc871e01 100644 --- a/docassemble/AssemblyLine/test_al_document.py +++ b/docassemble/AssemblyLine/test_al_document.py @@ -2,6 +2,9 @@ import json import pickle +import tempfile + +import pikepdf import unittest from unittest.mock import Mock from html import unescape @@ -701,3 +704,60 @@ def test_original_value_exceeds_trigger_with_newlines(self): if __name__ == "__main__": unittest.main() + + +class TestBackgroundBundlePdf(unittest.TestCase): + def make_bundle(self): + pdf = FakePdf() + bundle = ALDocumentBundle( + "bundle", elements=[FakeSingleDoc(pdf)], title="Bundle", filename="bundle" + ) + return bundle, pdf + + def test_opt_in_returns_background_pdf_without_evaluating_documents(self): + bundle, _ = self.make_bundle() + cached_pdf = FakePdf("cached.pdf") + bundle._downloadable_files = ([], None, cached_pdf) + with patch.object( + bundle, "enabled_documents", side_effect=AssertionError("regenerated") + ): + self.assertIs(bundle.as_pdf(use_previously_cached_files=True), cached_pdf) + + def test_default_keeps_foreground_behavior(self): + bundle, foreground = self.make_bundle() + bundle._downloadable_files = ([], None, FakePdf("cached.pdf")) + self.assertIs(bundle.as_pdf(), foreground) + + def test_absent_or_missing_background_pdf_falls_back(self): + for cached in [None, ([], None, None)]: + with self.subTest(cached=cached): + bundle, foreground = self.make_bundle() + if cached is not None: + bundle._downloadable_files = cached + self.assertIs( + bundle.as_pdf(use_previously_cached_files=True), foreground + ) + + def test_custom_versions_and_pdfa_do_not_reuse_unknown_cache_options(self): + for options in [{"key": "preview"}, {"pdfa": True}]: + with self.subTest(options=options): + bundle, foreground = self.make_bundle() + bundle._downloadable_files = ([], None, FakePdf("cached.pdf")) + self.assertIs( + bundle.as_pdf(use_previously_cached_files=True, **options), + foreground, + ) + + def test_parity_request_uses_foreground_pdf(self): + bundle, foreground = self.make_bundle() + bundle._downloadable_files = ([], None, FakePdf("cached.pdf")) + with tempfile.NamedTemporaryFile(suffix=".pdf") as pdf_file: + with pikepdf.Pdf.new() as pdf: + pdf.add_blank_page() + pdf.add_blank_page() + pdf.save(pdf_file.name) + foreground.path = lambda: pdf_file.name + self.assertIs( + bundle.as_pdf(use_previously_cached_files=True, ensure_parity="even"), + foreground, + )