mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Feature: page count (#7750)
--------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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', () => { | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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(() => { | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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> } | ||||
|   | ||||
| @@ -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', () => { | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -345,6 +345,7 @@ export class SettingsService { | ||||
|             DisplayField.CREATED, | ||||
|             DisplayField.ADDED, | ||||
|             DisplayField.ASN, | ||||
|             DisplayField.PAGE_COUNT, | ||||
|             DisplayField.SHARED, | ||||
|           ].includes(field.id) | ||||
|         ) { | ||||
|   | ||||
| @@ -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}") | ||||
|   | ||||
| @@ -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, | ||||
|         ) | ||||
|  | ||||
|   | ||||
| @@ -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("-"): | ||||
|   | ||||
							
								
								
									
										62
									
								
								src/documents/migrations/1053_document_page_count.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/documents/migrations/1053_document_page_count.py
									
									
									
									
									
										Normal 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, | ||||
|         ), | ||||
|     ] | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
							
								
								
									
										59
									
								
								src/documents/tests/test_migration_document_pages_count.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/documents/tests/test_migration_document_pages_count.py
									
									
									
									
									
										Normal 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()], | ||||
|         ) | ||||
| @@ -361,6 +361,7 @@ class DocumentViewSet( | ||||
|         "archive_serial_number", | ||||
|         "num_notes", | ||||
|         "owner", | ||||
|         "page_count", | ||||
|     ) | ||||
|  | ||||
|     def get_queryset(self): | ||||
|   | ||||
| @@ -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": | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 s0llvan
					s0llvan