Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions docassemble/AssemblyLine/al_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -3887,6 +3887,7 @@ def as_pdf(
add_cover_page: bool = True,
filename: Optional[str] = None,
append_matching_suffix: bool = True,
refresh_cover_page: Optional[bool] = None,
) -> Optional[DAFile]:
"""
Generates a PDF version of the exhibit, with optional features like Bates numbering or a cover page.
Expand All @@ -3896,7 +3897,7 @@ def as_pdf(
Note that these are keyword only parameters, not positional.

Args:
refresh (bool): If True, forces the exhibit to refresh before generating the PDF. (unused, provided for signature compatibility).
refresh (bool): If True, rebuilds the cover page and bypasses the cached exhibit PDF.
pdfa (bool): If True, the generated PDF will be in PDF/A format.
add_page_numbers (bool): If True, apply Bates numbering starting from 'self.start_page'.
page_number_prefix (str): If add_page_numbers is True, this gets added to the beginning on the bates number each page (e.g. `EX-`).
Expand All @@ -3908,6 +3909,8 @@ def as_pdf(
add_cover_page (bool): If True, prepend the exhibit with a cover page.
filename (Optional[str]): Custom filename for the generated PDF. Default is "exhibits.pdf".
append_matching_suffix (bool): If True, appends a suffix to the filename based on certain matching criteria.
refresh_cover_page (Optional[bool]): Whether to rebuild the cover page. Defaults to the value of `refresh`;
pass False when the caller has already refreshed it.

Returns:
DAFile: PDF representation of the exhibit.
Expand All @@ -3925,7 +3928,7 @@ def as_pdf(
if add_page_numbers:
safe_key = safe_key + "_page_nums"

if hasattr(self._cache, safe_key):
if not refresh and hasattr(self._cache, safe_key):
self._failed_during_processing = False
return getattr(self._cache, safe_key)
if not filename:
Expand All @@ -3936,6 +3939,10 @@ def as_pdf(
f"ALExhibit.as_pdf(): no valid pages for exhibit '{self.title}', skipping"
)
return None
if refresh_cover_page is None:
refresh_cover_page = refresh
if refresh_cover_page and add_cover_page:
self.getattr_fresh("cover_page")
try:
if add_cover_page:
concatenated_pages = pdf_concatenate(
Expand Down Expand Up @@ -4184,6 +4191,7 @@ def as_pdf(
page_number_offset_vertical: float = 15,
toc_pages: int = 0,
append_matching_suffix: bool = True,
refresh: bool = False,
) -> Optional[DAFile]:
"""
Compiles all exhibits in the list into a single PDF.
Expand All @@ -4202,6 +4210,8 @@ def as_pdf(
page_number_offset_vertical (float): The number of pixels that the bates page number is offset from the top / bottom of the page.
toc_pages (int): Expected number of pages in the table of contents.
append_matching_suffix (bool): If True, appends matching suffix to the filename.
refresh (bool): If True, recalculates auto-generated labels and page offsets, then rebuilds
exhibit cover pages and rendered files.

Returns:
DAFile: A single PDF containing all exhibits.
Expand All @@ -4213,17 +4223,31 @@ def as_pdf(
${ exhibit_attachment.exhibits.as_pdf() }
```
"""
if refresh:
# List edits can leave old labels and page offsets defined, and the
# cover pages depend on both.
if self.auto_label:
self._update_labels()
self._update_page_numbers(toc_guess_pages=toc_pages)
elif self.include_table_of_contents and toc_pages != 1:
self._update_page_numbers(toc_guess_pages=toc_pages)
if self.include_exhibit_cover_pages:
# Resolve every refreshed attachment before beginning expensive PDF
# rendering: dependency gathering can restart this method.
for exhibit in self:
exhibit.cover_page
if self.include_table_of_contents and toc_pages != 1:
self._update_page_numbers(toc_guess_pages=toc_pages)
if refresh:
exhibit.getattr_fresh("cover_page")
else:
exhibit.cover_page
if not page_number_prefix and self.bates_prefix:
page_number_prefix = self.bates_prefix
exhibit_pdfs = [
pdf
for pdf in (
exhibit.as_pdf(
refresh=refresh,
# Already refreshed above
refresh_cover_page=False,
add_cover_page=self.include_exhibit_cover_pages,
add_page_numbers=add_page_numbers,
page_number_prefix=page_number_prefix,
Expand Down Expand Up @@ -4636,7 +4660,15 @@ def as_pdf(

if len(self.exhibits):
self._set_default_attributes()
# List edits can leave old labels, page offsets and attachment
# objects defined. Refresh their dependencies before rendering.
self.exhibits._update_page_numbers()
if self.exhibits.auto_label:
self.exhibits._update_labels()
if refresh and self.include_table_of_contents:
self.getattr_fresh("table_of_contents")
exhibits_pdf = self.exhibits.as_pdf(
refresh=refresh,
add_page_numbers=self.add_page_numbers,
page_number_prefix=self.page_number_prefix,
page_number_digits=self.page_number_digits,
Expand Down
114 changes: 114 additions & 0 deletions docassemble/AssemblyLine/test_al_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ALDocumentBundle,
ALExhibit,
ALExhibitList,
ALExhibitDocument,
_javascript_href,
)
from docassemble.base.error import DAError
Expand Down Expand Up @@ -686,5 +687,118 @@ def test_original_value_exceeds_trigger_with_newlines(self):
) # Original value exceeds the overflow_trigger, but preserve_newlines is True


class TestExhibitReviewRefresh(unittest.TestCase):
def test_changed_order_updates_labels_offsets_and_table_of_contents(self):
document = ALExhibitDocument("appendix", title="Appendix", filename="appendix")
for title in ["Renamed beta", "Gamma"]:
exhibit = document.exhibits.appendObject(title=title)
exhibit.pages = Mock()
exhibit.pages.num_pages.return_value = 1
exhibit.label = "stale"
exhibit.start_page = 99
document.exhibits.gathered = True
toc = Mock()
toc.num_pages.return_value = 1
document.table_of_contents = toc
with (
patch.object(document, "getattr_fresh", return_value=toc) as refresh,
patch.object(
document.exhibits, "as_pdf", return_value="exhibits"
) as render,
patch(
__package__ + ".al_document.pdf_concatenate", return_value="combined"
) as combine,
):
self.assertEqual(document.as_pdf(), "combined")
self.assertEqual([item.label for item in document.exhibits], ["A", "B"])
self.assertEqual([item.start_page for item in document.exhibits], [2, 4])
refresh.assert_called_once_with("table_of_contents")
self.assertTrue(render.call_args.kwargs["refresh"])
combine.assert_called_once()

def test_refresh_bypasses_old_exhibit_pdf_and_regenerates_cover(self):
exhibit = ALExhibit("exhibit", title="Renamed exhibit", start_page=2)
exhibit.cover_page = "old cover"
exhibit._cache._file = "old rendered exhibit"
new_pdf = Mock()
with (
patch.object(
exhibit,
"getattr_fresh",
side_effect=lambda name: setattr(exhibit, name, "new cover"),
) as cover,
patch.object(exhibit, "ocr_pages", return_value=[Mock(ok=True)]),
patch(
__package__ + ".al_document.pdf_concatenate", return_value=new_pdf
) as combine,
):
self.assertEqual(
exhibit.as_pdf(refresh=False, add_page_numbers=False),
"old rendered exhibit",
)
combine.assert_not_called()
self.assertIs(exhibit.as_pdf(refresh=True, add_page_numbers=False), new_pdf)
cover.assert_called_once_with("cover_page")
combine.assert_called_once()
self.assertEqual(combine.call_args.args[0], "new cover")

def test_refresh_resolves_all_covers_before_rendering_any_pdf(self):
events = []
exhibits = []
for index in range(2):
exhibit = Mock()
exhibit.num_pages.return_value = 2
exhibit.label = "stale"
exhibit.start_page = 99
exhibit.getattr_fresh.side_effect = lambda name, i=index: events.append(
("refresh", i)
)
exhibit.as_pdf.side_effect = lambda i=index, **kwargs: (
events.append(("render", i)) or "rendered"
)
exhibits.append(exhibit)
collection = ALExhibitList(
"exhibits",
elements=exhibits,
gathered=True,
include_table_of_contents=False,
)
with patch(__package__ + ".al_document.pdf_concatenate"):
collection.as_pdf(refresh=True)
self.assertEqual(
events,
[("refresh", 0), ("refresh", 1), ("render", 0), ("render", 1)],
)
# A direct list-level refresh recalculates labels and offsets itself
self.assertEqual([item.label for item in exhibits], ["A", "B"])
self.assertEqual([item.start_page for item in exhibits], [1, 4])
# Covers refreshed in the first pass are not rebuilt again per exhibit
for exhibit in exhibits:
exhibit.getattr_fresh.assert_called_once_with("cover_page")
self.assertTrue(exhibit.as_pdf.call_args.kwargs["refresh"])
self.assertIs(exhibit.as_pdf.call_args.kwargs["refresh_cover_page"], False)

def test_refresh_cover_page_false_keeps_cover_but_bypasses_cache(self):
exhibit = ALExhibit("exhibit", title="Exhibit", start_page=2)
exhibit.cover_page = "refreshed cover"
exhibit._cache._file = "old rendered exhibit"
new_pdf = Mock()
with (
patch.object(exhibit, "getattr_fresh") as cover,
patch.object(exhibit, "ocr_pages", return_value=[Mock(ok=True)]),
patch(
__package__ + ".al_document.pdf_concatenate", return_value=new_pdf
) as combine,
):
self.assertIs(
exhibit.as_pdf(
refresh=True, refresh_cover_page=False, add_page_numbers=False
),
new_pdf,
)
cover.assert_not_called()
self.assertEqual(combine.call_args.args[0], "refreshed cover")


if __name__ == "__main__":
unittest.main()
Loading