Feature: page count (#7750)

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
s0llvan 2024-09-25 17:22:12 +02:00 committed by GitHub
parent 4adf20af1e
commit c92c3e224a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 319 additions and 45 deletions

View File

@ -54,6 +54,7 @@ fields:
- `archived_file_name`: Verbose filename of the archived document.
Read-only. Null if no archived document is available.
- `notes`: Array of notes associated with the document.
- `page_count`: Number of pages.
- `set_permissions`: Allows setting document permissions. Optional,
write-only. See [below](#permissions).
- `custom_fields`: Array of custom fields & values, specified as

View File

@ -1046,11 +1046,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">62</context>
<context context-type="linenumber">63</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">95</context>
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="293524471897878391" datatype="html">
@ -1954,11 +1954,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">38</context>
<context context-type="linenumber">39</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">92</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit id="5968132631442328843" datatype="html">
@ -2414,7 +2414,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">120</context>
<context context-type="linenumber">128</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@ -2760,7 +2760,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">47</context>
</context-group>
</trans-unit>
<trans-unit id="4369111787961525769" datatype="html">
@ -2972,7 +2972,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit id="searchResults.noResults" datatype="html">
@ -3345,11 +3345,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">42</context>
<context context-type="linenumber">43</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">98</context>
</context-group>
</trans-unit>
<trans-unit id="4873149362496451858" datatype="html">
@ -5498,7 +5498,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">277</context>
<context context-type="linenumber">286</context>
</context-group>
</trans-unit>
<trans-unit id="78870852467682010" datatype="html">
@ -5513,7 +5513,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">312</context>
<context context-type="linenumber">321</context>
</context-group>
</trans-unit>
<trans-unit id="157572966557284263" datatype="html">
@ -5528,7 +5528,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">319</context>
<context context-type="linenumber">328</context>
</context-group>
</trans-unit>
<trans-unit id="8911158217491828773" datatype="html">
@ -5826,11 +5826,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">35</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">90</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="1379170675585571971" datatype="html">
@ -5867,11 +5867,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">50</context>
<context context-type="linenumber">51</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">89</context>
<context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="5066119607229701477" datatype="html">
@ -5894,11 +5894,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">54</context>
<context context-type="linenumber">55</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">91</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="2091353339965748767" datatype="html">
@ -5921,7 +5921,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">58</context>
<context context-type="linenumber">59</context>
</context-group>
</trans-unit>
<trans-unit id="6205355627445317276" datatype="html">
@ -6714,7 +6714,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">288</context>
<context context-type="linenumber">297</context>
</context-group>
</trans-unit>
<trans-unit id="106713086593101376" datatype="html">
@ -6769,19 +6769,30 @@
<context context-type="linenumber">82,83</context>
</context-group>
</trans-unit>
<trans-unit id="197162226430950645" datatype="html">
<source>{VAR_PLURAL, plural, =1 {1 page} other {<x id="INTERPOLATION"/> pages}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">117</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="5739581984228459958" datatype="html">
<source>Shared</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">127</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">106</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">71</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/username.pipe.ts</context>
@ -6792,7 +6803,7 @@
<source>Score:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="3661756380991326939" datatype="html">
@ -6931,11 +6942,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">74</context>
<context context-type="linenumber">75</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">88</context>
<context context-type="linenumber">93</context>
</context-group>
</trans-unit>
<trans-unit id="6954625430271090777" datatype="html">
@ -6967,11 +6978,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">66</context>
<context context-type="linenumber">67</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">96</context>
<context context-type="linenumber">101</context>
</context-group>
</trans-unit>
<trans-unit id="3557446856808034218" datatype="html">
@ -7009,25 +7020,51 @@
<context context-type="linenumber">243</context>
</context-group>
</trans-unit>
<trans-unit id="4874754501044009042" datatype="html">
<source>Sort by number of pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">252</context>
</context-group>
</trans-unit>
<trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">256</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">79</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">102</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">90</context>
</context-group>
</trans-unit>
<trans-unit id="329406837759048287" datatype="html">
<source> Shared </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">250,252</context>
<context context-type="linenumber">259,261</context>
</context-group>
</trans-unit>
<trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">284</context>
<context context-type="linenumber">293</context>
</context-group>
</trans-unit>
<trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">335</context>
<context context-type="linenumber">349</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
@ -7038,7 +7075,7 @@
<source>No</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">335</context>
<context context-type="linenumber">349</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
@ -7972,14 +8009,14 @@
<source>Modified</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">94</context>
<context context-type="linenumber">99</context>
</context-group>
</trans-unit>
<trans-unit id="4460262093225954455" datatype="html">
<source>Search score</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
<context context-type="linenumber">102</context>
<context context-type="linenumber">108</context>
</context-group>
<note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note>
</trans-unit>
@ -8095,13 +8132,6 @@
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
<context context-type="linenumber">90</context>
</context-group>
</trans-unit>
<trans-unit id="1713271461473302108" datatype="html">
<source>Mode</source>
<context-group purpose="location">

View File

@ -65,6 +65,7 @@ const savedView: SavedView = {
DisplayField.CORRESPONDENT,
DisplayField.DOCUMENT_TYPE,
DisplayField.STORAGE_PATH,
DisplayField.PAGE_COUNT,
`${DisplayField.CUSTOM_FIELD}11` as any,
`${DisplayField.CUSTOM_FIELD}15` as any,
],
@ -344,6 +345,7 @@ describe('SavedViewWidgetComponent', () => {
expect(component.getColumnTitle(DisplayField.STORAGE_PATH)).toEqual(
'Storage path'
)
expect(component.getColumnTitle(DisplayField.PAGE_COUNT)).toEqual('Pages')
})
it('should get correct column title for custom field', () => {

View File

@ -111,6 +111,12 @@
</div>
}
}
@if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="files"></i-bs>
<small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
</div>
}
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>

View File

@ -31,6 +31,7 @@ const doc = {
correspondent: 8,
document_type: 10,
storage_path: null,
page_count: 8,
notes: [
{
id: 11,
@ -80,6 +81,7 @@ describe('DocumentCardLargeComponent', () => {
it('should display a document', () => {
expect(fixture.nativeElement.textContent).toContain('Document 10')
expect(fixture.nativeElement.textContent).toContain('Cupcake ipsum')
expect(fixture.nativeElement.textContent).toContain('8 pages')
})
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {

View File

@ -88,6 +88,14 @@
</div>
</div>
}
@if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<div class="ps-0 p-1" placement="top">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="files"></i-bs>
<small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>

View File

@ -34,6 +34,7 @@ const doc = {
correspondent: 8,
document_type: 10,
storage_path: null,
page_count: 12,
notes: [
{
id: 11,
@ -91,6 +92,10 @@ describe('DocumentCardSmallComponent', () => {
fixture.detectChanges()
})
it('should display page count', () => {
expect(fixture.nativeElement.textContent).toContain('12 pages')
})
it('should display a document, limit tags to 5', () => {
expect(fixture.nativeElement.textContent).toContain('Document 10')
expect(

View File

@ -246,6 +246,15 @@
(sort)="onSort($event)"
i18n>Added</th>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
<th class="cursor-pointer"
pngxSortable="page_count"
title="Sort by number of pages" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Pages</th>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<th i18n>
Shared
@ -330,6 +339,11 @@
{{d.added | customDate}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
<td>
{{ d.page_count }}
</td>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<td>
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }

View File

@ -602,7 +602,7 @@ describe('DocumentListComponent', () => {
expect(
fixture.debugElement.queryAll(By.directive(SortableDirective))
).toHaveLength(9)
).toHaveLength(10)
expect(component.notesEnabled).toBeTruthy()
settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false)
@ -610,14 +610,14 @@ describe('DocumentListComponent', () => {
expect(component.notesEnabled).toBeFalsy()
expect(
fixture.debugElement.queryAll(By.directive(SortableDirective))
).toHaveLength(8)
).toHaveLength(9)
// insufficient perms
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(false)
fixture.detectChanges()
expect(
fixture.debugElement.queryAll(By.directive(SortableDirective))
).toHaveLength(4)
).toHaveLength(5)
})
it('should support toggle on document objects', () => {

View File

@ -26,6 +26,7 @@ export enum DisplayField {
OWNER = 'owner',
SHARED = 'shared',
ASN = 'asn',
PAGE_COUNT = 'pagecount',
}
export const DEFAULT_DISPLAY_FIELDS = [
@ -73,6 +74,10 @@ export const DEFAULT_DISPLAY_FIELDS = [
id: DisplayField.ASN,
name: $localize`ASN`,
},
{
id: DisplayField.PAGE_COUNT,
name: $localize`Pages`,
},
]
export const DEFAULT_DASHBOARD_VIEW_PAGE_SIZE = 10
@ -94,6 +99,7 @@ export const DOCUMENT_SORT_FIELDS = [
{ field: 'modified', name: $localize`Modified` },
{ field: 'num_notes', name: $localize`Notes` },
{ field: 'owner', name: $localize`Owner` },
{ field: 'page_count', name: $localize`Pages` },
]
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [
@ -164,4 +170,6 @@ export interface Document extends ObjectWithPermissions {
// write-only field
remove_inbox_tags?: boolean
page_count?: number
}

View File

@ -345,6 +345,7 @@ export class SettingsService {
DisplayField.CREATED,
DisplayField.ADDED,
DisplayField.ASN,
DisplayField.PAGE_COUNT,
DisplayField.SHARED,
].includes(field.id)
) {

View File

@ -387,6 +387,8 @@ def delete_pages(doc_ids: list[int], pages: list[int]):
pdf.remove_unreferenced_resources()
pdf.save()
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
if doc.page_count is not None:
doc.page_count = doc.page_count - len(pages)
doc.save()
update_document_archive_file.delay(document_id=doc.id)
logger.info(f"Deleted pages {pages} from document {doc.id}")

View File

@ -586,6 +586,7 @@ class ConsumerPlugin(
date = None
thumbnail = None
archive_path = None
page_count = None
try:
self._send_progress(
@ -621,6 +622,7 @@ class ConsumerPlugin(
)
date = parse_date(self.filename, text)
archive_path = document_parser.get_archive_path()
page_count = document_parser.get_page_count(self.working_copy, mime_type)
except ParseError as e:
document_parser.cleanup()
@ -662,7 +664,12 @@ class ConsumerPlugin(
try:
with transaction.atomic():
# store the document.
document = self._store(text=text, date=date, mime_type=mime_type)
document = self._store(
text=text,
date=date,
page_count=page_count,
mime_type=mime_type,
)
# If we get here, it was successful. Proceed with post-consume
# hooks. If they fail, nothing will get changed.
@ -790,6 +797,7 @@ class ConsumerPlugin(
self,
text: str,
date: Optional[datetime.datetime],
page_count: Optional[int],
mime_type: str,
) -> Document:
# If someone gave us the original filename, use it instead of doc.
@ -835,6 +843,7 @@ class ConsumerPlugin(
created=create_date,
modified=create_date,
storage_type=storage_type,
page_count=page_count,
original_filename=self.filename,
)

View File

@ -80,6 +80,7 @@ def get_schema():
has_owner=BOOLEAN(),
viewer_id=KEYWORD(commas=True),
checksum=TEXT(),
page_count=NUMERIC(sortable=True),
original_filename=TEXT(sortable=True),
is_shared=BOOLEAN(),
)
@ -181,6 +182,7 @@ def update_document(writer: AsyncWriter, doc: Document):
has_owner=doc.owner is not None,
viewer_id=viewer_ids if viewer_ids else None,
checksum=doc.checksum,
page_count=doc.page_count,
original_filename=doc.original_filename,
is_shared=len(viewer_ids) > 0,
)
@ -247,6 +249,7 @@ class DelayedQuery:
"archive_serial_number": "asn",
"num_notes": "num_notes",
"owner": "owner",
"page_count": "page_count",
}
if field.startswith("-"):

View File

@ -0,0 +1,62 @@
# Generated by Django 4.2.16 on 2024-09-21 15:44
from pathlib import Path
import pikepdf
from django.conf import settings
from django.db import migrations
from django.db import models
from django.utils.termcolors import colorize as colourise
def source_path(self):
if self.filename:
fname = str(self.filename)
return Path(settings.ORIGINALS_DIR / fname).resolve()
def add_number_of_pages_to_page_count(apps, schema_editor):
Document = apps.get_model("documents", "Document")
if not Document.objects.all().exists():
return
for doc in Document.objects.filter(mime_type="application/pdf"):
print(
" {} {} {}".format(
colourise("*", fg="green"),
colourise("Calculating number of pages for", fg="white"),
colourise(doc.filename, fg="cyan"),
),
)
try:
with pikepdf.Pdf.open(source_path(doc)) as pdf:
if pdf.pages is not None:
doc.page_count = len(pdf.pages)
doc.save()
except Exception as e: # pragma: no cover
print(f"Error retrieving number of pages for {doc.filename}: {e}")
class Migration(migrations.Migration):
dependencies = [
("documents", "1052_document_transaction_id"),
]
operations = [
migrations.AddField(
model_name="document",
name="page_count",
field=models.PositiveIntegerField(
blank=False,
null=True,
unique=False,
db_index=False,
),
),
migrations.RunPython(
add_number_of_pages_to_page_count,
migrations.RunPython.noop,
),
]

View File

@ -205,6 +205,18 @@ class Document(SoftDeleteModel, ModelWithOwner):
help_text=_("The checksum of the archived document."),
)
page_count = models.PositiveIntegerField(
_("page count"),
blank=False,
null=True,
unique=False,
db_index=False,
validators=[MinValueValidator(1)],
help_text=_(
"The number of pages of the document.",
),
)
created = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
modified = models.DateTimeField(
@ -414,6 +426,7 @@ class SavedView(ModelWithOwner):
OWNER = ("owner", _("Owner"))
SHARED = ("shared", _("Shared"))
ASN = ("asn", _("ASN"))
PAGE_COUNT = ("pagecount", _("Pages"))
CUSTOM_FIELD = ("custom_field_%d", ("Custom Field"))
name = models.CharField(_("name"), max_length=128)

View File

@ -367,6 +367,9 @@ class DocumentParser(LoggingMixin):
def extract_metadata(self, document_path, mime_type):
return []
def get_page_count(self, document_path, mime_type):
return None
def parse(self, document_path, mime_type, file_name=None):
raise NotImplementedError

View File

@ -750,6 +750,7 @@ class DocumentSerializer(
original_file_name = SerializerMethodField()
archived_file_name = SerializerMethodField()
created_date = serializers.DateField(required=False)
page_count = SerializerMethodField()
custom_fields = CustomFieldInstanceSerializer(
many=True,
@ -770,6 +771,9 @@ class DocumentSerializer(
required=False,
)
def get_page_count(self, obj):
return obj.page_count
def get_original_file_name(self, obj):
return obj.original_filename
@ -885,6 +889,7 @@ class DocumentSerializer(
"notes",
"custom_fields",
"remove_inbox_tags",
"page_count",
)
list_serializer_class = OwnedObjectListSerializer

View File

@ -389,6 +389,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
title="B",
filename=sample2,
mime_type="application/pdf",
page_count=8,
)
self.doc2.archive_filename = sample2_archive
self.doc2.save()
@ -681,14 +682,20 @@ class TestPDFActions(DirectoriesMixin, TestCase):
THEN:
- Save should be called once
- Archive file should be updated once
- The document's page_count should be reduced by the number of deleted pages
"""
doc_ids = [self.doc2.id]
initial_page_count = self.doc2.page_count
pages = [1, 3]
result = bulk_edit.delete_pages(doc_ids, pages)
mock_pdf_save.assert_called_once()
mock_update_archive_file.assert_called_once()
self.assertEqual(result, "OK")
expected_page_count = initial_page_count - len(pages)
self.doc2.refresh_from_db()
self.assertEqual(self.doc2.page_count, expected_page_count)
@mock.patch("documents.tasks.update_document_archive_file.delay")
@mock.patch("pikepdf.Pdf.save")
def test_delete_pages_with_error(self, mock_pdf_save, mock_update_archive_file):

View File

@ -0,0 +1,59 @@
import os
import shutil
from pathlib import Path
from django.conf import settings
from documents.tests.utils import TestMigrations
def source_path_before(self):
if self.filename:
fname = str(self.filename)
return os.path.join(settings.ORIGINALS_DIR, fname)
class TestMigrateDocumentPageCount(TestMigrations):
migrate_from = "1052_document_transaction_id"
migrate_to = "1053_document_page_count"
def setUpBeforeMigration(self, apps):
Document = apps.get_model("documents", "Document")
doc = Document.objects.create(
title="test1",
mime_type="application/pdf",
filename="file1.pdf",
)
self.doc_id = doc.id
shutil.copy(
Path(__file__).parent / "samples" / "simple.pdf",
source_path_before(doc),
)
def testDocumentPageCountMigrated(self):
Document = self.apps.get_model("documents", "Document")
doc = Document.objects.get(id=self.doc_id)
self.assertEqual(doc.page_count, 1)
class TestMigrateDocumentPageCountBackwards(TestMigrations):
migrate_from = "1053_document_page_count"
migrate_to = "1052_document_transaction_id"
def setUpBeforeMigration(self, apps):
Document = apps.get_model("documents", "Document")
doc = Document.objects.create(
title="test1",
mime_type="application/pdf",
filename="file1.pdf",
page_count=8,
)
self.doc_id = doc.id
def test_remove_number_of_pages_to_page_count(self):
Document = self.apps.get_model("documents", "Document")
self.assertFalse(
"page_count" in [field.name for field in Document._meta.get_fields()],
)

View File

@ -361,6 +361,7 @@ class DocumentViewSet(
"archive_serial_number",
"num_notes",
"owner",
"page_count",
)
def get_queryset(self):

View File

@ -41,6 +41,15 @@ class RasterisedDocumentParser(DocumentParser):
"""
return OcrConfig()
def get_page_count(self, document_path, mime_type):
page_count = None
if mime_type == "application/pdf":
import pikepdf
with pikepdf.Pdf.open(document_path) as pdf:
page_count = len(pdf.pages)
return page_count
def extract_metadata(self, document_path, mime_type):
result = []
if mime_type == "application/pdf":

View File

@ -57,6 +57,30 @@ class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertContainsStrings(text.strip(), ["This is a test document."])
def test_get_page_count(self):
"""
GIVEN:
- PDF file with a single page
- PDF file with multiple pages
WHEN:
- The number of pages is requested
THEN:
- The method returns 1 as the expected number of pages
- The method returns the correct number of pages (6)
"""
parser = RasterisedDocumentParser(uuid.uuid4())
page_count = parser.get_page_count(
os.path.join(self.SAMPLE_FILES, "simple-digital.pdf"),
"application/pdf",
)
self.assertEqual(page_count, 1)
page_count = parser.get_page_count(
os.path.join(self.SAMPLE_FILES, "multi-page-mixed.pdf"),
"application/pdf",
)
self.assertEqual(page_count, 6)
def test_thumbnail(self):
parser = RasterisedDocumentParser(uuid.uuid4())
thumb = parser.get_thumbnail(