mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			1002d37f6b
			...
			feature-re
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ec12e71487 | ||
| 
						 | 
					62b470f691 | ||
| 
						 | 
					a2e4977201 | ||
| 
						 | 
					0fcd69b739 | ||
| 
						 | 
					af1c64e969 | ||
| 
						 | 
					85c661dff2 | ||
| 
						 | 
					3a7eee2c2e | ||
| 
						 | 
					bc4d3925cc | 
@@ -18,6 +18,7 @@
 | 
				
			|||||||
# Paths and folders
 | 
					# Paths and folders
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#PAPERLESS_CONSUMPTION_DIR=../consume
 | 
					#PAPERLESS_CONSUMPTION_DIR=../consume
 | 
				
			||||||
 | 
					#PAPERLESS_CONSUMPTION_FAILED_DIR=../consume/failed
 | 
				
			||||||
#PAPERLESS_DATA_DIR=../data
 | 
					#PAPERLESS_DATA_DIR=../data
 | 
				
			||||||
#PAPERLESS_EMPTY_TRASH_DIR=
 | 
					#PAPERLESS_EMPTY_TRASH_DIR=
 | 
				
			||||||
#PAPERLESS_MEDIA_ROOT=../media
 | 
					#PAPERLESS_MEDIA_ROOT=../media
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1994,120 +1994,141 @@
 | 
				
			|||||||
          <context context-type="linenumber">72</context>
 | 
					          <context context-type="linenumber">72</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="7934833136974560675" datatype="html">
 | 
				
			||||||
 | 
					        <source>Retry</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">86</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="1536087519743707362" datatype="html">
 | 
					      <trans-unit id="1536087519743707362" datatype="html">
 | 
				
			||||||
        <source>Dismiss</source>
 | 
					        <source>Dismiss</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">85</context>
 | 
					          <context context-type="linenumber">90</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">68</context>
 | 
					          <context context-type="linenumber">71</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="2134950584701094962" datatype="html">
 | 
					      <trans-unit id="2134950584701094962" datatype="html">
 | 
				
			||||||
        <source>Open Document</source>
 | 
					        <source>Open Document</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">90</context>
 | 
					          <context context-type="linenumber">95</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="428536141871853903" datatype="html">
 | 
					      <trans-unit id="428536141871853903" datatype="html">
 | 
				
			||||||
        <source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/> task} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION"/> tasks}}</source>
 | 
					        <source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/> task} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION"/> tasks}}</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">109</context>
 | 
					          <context context-type="linenumber">114</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="1943508481059904274" datatype="html">
 | 
					      <trans-unit id="1943508481059904274" datatype="html">
 | 
				
			||||||
        <source> (<x id="INTERPOLATION" equiv-text="{{selectedTasks.size}}"/> selected)</source>
 | 
					        <source> (<x id="INTERPOLATION" equiv-text="{{selectedTasks.size}}"/> selected)</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">111</context>
 | 
					          <context context-type="linenumber">116</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="5639839509673911668" datatype="html">
 | 
					      <trans-unit id="5639839509673911668" datatype="html">
 | 
				
			||||||
        <source>Failed<x id="START_BLOCK_IF" equiv-text="@if (tasksService.failedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-danger ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
					        <source>Failed<x id="START_BLOCK_IF" equiv-text="@if (tasksService.failedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-danger ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">123,125</context>
 | 
					          <context context-type="linenumber">128,130</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8210778930307085868" datatype="html">
 | 
					      <trans-unit id="8210778930307085868" datatype="html">
 | 
				
			||||||
        <source>Complete<x id="START_BLOCK_IF" equiv-text="@if (tasksService.completedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.completedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
					        <source>Complete<x id="START_BLOCK_IF" equiv-text="@if (tasksService.completedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.completedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">131,133</context>
 | 
					          <context context-type="linenumber">136,138</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="3522801015717851360" datatype="html">
 | 
					      <trans-unit id="3522801015717851360" datatype="html">
 | 
				
			||||||
        <source>Started<x id="START_BLOCK_IF" equiv-text="@if (tasksService.startedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
					        <source>Started<x id="START_BLOCK_IF" equiv-text="@if (tasksService.startedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">139,141</context>
 | 
					          <context context-type="linenumber">144,146</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="2341807459308874922" datatype="html">
 | 
					      <trans-unit id="2341807459308874922" datatype="html">
 | 
				
			||||||
        <source>Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
					        <source>Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">147,149</context>
 | 
					          <context context-type="linenumber">152,154</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="5404910960991552159" datatype="html">
 | 
					      <trans-unit id="5404910960991552159" datatype="html">
 | 
				
			||||||
        <source>Dismiss selected</source>
 | 
					        <source>Dismiss selected</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">31</context>
 | 
					          <context context-type="linenumber">33</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8829078752502782653" datatype="html">
 | 
					      <trans-unit id="8829078752502782653" datatype="html">
 | 
				
			||||||
        <source>Dismiss all</source>
 | 
					        <source>Dismiss all</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">32</context>
 | 
					          <context context-type="linenumber">34</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="1323591410517879795" datatype="html">
 | 
					      <trans-unit id="1323591410517879795" datatype="html">
 | 
				
			||||||
        <source>Confirm Dismiss All</source>
 | 
					        <source>Confirm Dismiss All</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">65</context>
 | 
					          <context context-type="linenumber">68</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="4157200209636243740" datatype="html">
 | 
					      <trans-unit id="4157200209636243740" datatype="html">
 | 
				
			||||||
        <source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source>
 | 
					        <source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">66</context>
 | 
					          <context context-type="linenumber">69</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="7611027432301841688" datatype="html">
 | 
				
			||||||
 | 
					        <source>Retrying task...</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">92</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="5445438607105804721" datatype="html">
 | 
				
			||||||
 | 
					        <source>Failed to retry task</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">95</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="9011556615675272238" datatype="html">
 | 
					      <trans-unit id="9011556615675272238" datatype="html">
 | 
				
			||||||
        <source>queued</source>
 | 
					        <source>queued</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">135</context>
 | 
					          <context context-type="linenumber">149</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="6415892379431855826" datatype="html">
 | 
					      <trans-unit id="6415892379431855826" datatype="html">
 | 
				
			||||||
        <source>started</source>
 | 
					        <source>started</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">137</context>
 | 
					          <context context-type="linenumber">151</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="7510279840486540181" datatype="html">
 | 
					      <trans-unit id="7510279840486540181" datatype="html">
 | 
				
			||||||
        <source>completed</source>
 | 
					        <source>completed</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">139</context>
 | 
					          <context context-type="linenumber">153</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="4083337005045748464" datatype="html">
 | 
					      <trans-unit id="4083337005045748464" datatype="html">
 | 
				
			||||||
        <source>failed</source>
 | 
					        <source>failed</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">141</context>
 | 
					          <context context-type="linenumber">155</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="3418677553313974490" datatype="html">
 | 
					      <trans-unit id="3418677553313974490" datatype="html">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -81,6 +81,9 @@
 | 
				
			|||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
          <td scope="row">
 | 
					          <td scope="row">
 | 
				
			||||||
            <div class="btn-group" role="group">
 | 
					            <div class="btn-group" role="group">
 | 
				
			||||||
 | 
					              @if (task.status === PaperlessTaskStatus.Failed) {
 | 
				
			||||||
 | 
					                <ng-container *ngTemplateOutlet="retryDropdown; context: { task: task }"></ng-container>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
              <button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
 | 
					              <button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
 | 
				
			||||||
                <i-bs name="check"></i-bs> <ng-container i18n>Dismiss</ng-container>
 | 
					                <i-bs name="check"></i-bs> <ng-container i18n>Dismiss</ng-container>
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
@@ -153,3 +156,25 @@
 | 
				
			|||||||
  </li>
 | 
					  </li>
 | 
				
			||||||
</ul>
 | 
					</ul>
 | 
				
			||||||
<div [ngbNavOutlet]="nav"></div>
 | 
					<div [ngbNavOutlet]="nav"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<ng-template #retryDropdown let-task="task">
 | 
				
			||||||
 | 
					  <div ngbDropdown>
 | 
				
			||||||
 | 
					    <button class="btn btn-sm btn-outline-primary" (click)="$event.stopImmediatePropagation()" ngbDropdownToggle>
 | 
				
			||||||
 | 
					      <i-bs name="arrow-repeat"></i-bs> <ng-container i18n>Retry</ng-container>
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					    <div ngbDropdownMenu class="shadow retry-dropdown">
 | 
				
			||||||
 | 
					      <div class="p-2">
 | 
				
			||||||
 | 
					        <ul class="list-group list-group-flush">
 | 
				
			||||||
 | 
					            <li class="list-group-item small" i18n>
 | 
				
			||||||
 | 
					              <pngx-input-check [(ngModel)]="retryClean" i18n-title title="Attempt to clean pdf"></pngx-input-check>
 | 
				
			||||||
 | 
					            </li>
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					        <div class="d-flex justify-content-end">
 | 
				
			||||||
 | 
					          <button class="btn btn-sm btn-outline-primary" (click)="retryTask(task); $event.stopPropagation();">
 | 
				
			||||||
 | 
					            <ng-container i18n>Proceed</ng-container>
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</ng-template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,3 +26,7 @@ pre {
 | 
				
			|||||||
        max-width: 150px;
 | 
					        max-width: 150px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.retry-dropdown {
 | 
				
			||||||
 | 
					    width: 300px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,9 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
 | 
				
			|||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
					import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
				
			||||||
import { FormsModule } from '@angular/forms'
 | 
					import { FormsModule } from '@angular/forms'
 | 
				
			||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
					import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
				
			||||||
 | 
					import { ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
 | 
					import { of, throwError } from 'rxjs'
 | 
				
			||||||
 | 
					import { CheckComponent } from '../../common/input/check/check.component'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tasks: PaperlessTask[] = [
 | 
					const tasks: PaperlessTask[] = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
@@ -115,6 +118,7 @@ describe('TasksComponent', () => {
 | 
				
			|||||||
  let modalService: NgbModal
 | 
					  let modalService: NgbModal
 | 
				
			||||||
  let router: Router
 | 
					  let router: Router
 | 
				
			||||||
  let httpTestingController: HttpTestingController
 | 
					  let httpTestingController: HttpTestingController
 | 
				
			||||||
 | 
					  let toastService: ToastService
 | 
				
			||||||
  let reloadSpy
 | 
					  let reloadSpy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
@@ -125,6 +129,7 @@ describe('TasksComponent', () => {
 | 
				
			|||||||
        IfPermissionsDirective,
 | 
					        IfPermissionsDirective,
 | 
				
			||||||
        CustomDatePipe,
 | 
					        CustomDatePipe,
 | 
				
			||||||
        ConfirmDialogComponent,
 | 
					        ConfirmDialogComponent,
 | 
				
			||||||
 | 
					        CheckComponent,
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      imports: [
 | 
					      imports: [
 | 
				
			||||||
        NgbModule,
 | 
					        NgbModule,
 | 
				
			||||||
@@ -152,6 +157,7 @@ describe('TasksComponent', () => {
 | 
				
			|||||||
    httpTestingController = TestBed.inject(HttpTestingController)
 | 
					    httpTestingController = TestBed.inject(HttpTestingController)
 | 
				
			||||||
    modalService = TestBed.inject(NgbModal)
 | 
					    modalService = TestBed.inject(NgbModal)
 | 
				
			||||||
    router = TestBed.inject(Router)
 | 
					    router = TestBed.inject(Router)
 | 
				
			||||||
 | 
					    toastService = TestBed.inject(ToastService)
 | 
				
			||||||
    fixture = TestBed.createComponent(TasksComponent)
 | 
					    fixture = TestBed.createComponent(TasksComponent)
 | 
				
			||||||
    component = fixture.componentInstance
 | 
					    component = fixture.componentInstance
 | 
				
			||||||
    jest.useFakeTimers()
 | 
					    jest.useFakeTimers()
 | 
				
			||||||
@@ -173,8 +179,10 @@ describe('TasksComponent', () => {
 | 
				
			|||||||
      `Failed${currentTasksLength}`
 | 
					      `Failed${currentTasksLength}`
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    expect(
 | 
					    expect(
 | 
				
			||||||
      fixture.debugElement.queryAll(By.css('table input[type="checkbox"]'))
 | 
					      fixture.debugElement.queryAll(
 | 
				
			||||||
    ).toHaveLength(currentTasksLength + 1)
 | 
					        By.css('table td > .form-check input[type="checkbox"]')
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    ).toHaveLength(currentTasksLength)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    currentTasksLength = tasks.filter(
 | 
					    currentTasksLength = tasks.filter(
 | 
				
			||||||
      (t) => t.status === PaperlessTaskStatus.Complete
 | 
					      (t) => t.status === PaperlessTaskStatus.Complete
 | 
				
			||||||
@@ -289,4 +297,20 @@ describe('TasksComponent', () => {
 | 
				
			|||||||
    jest.advanceTimersByTime(6000)
 | 
					    jest.advanceTimersByTime(6000)
 | 
				
			||||||
    expect(reloadSpy).toHaveBeenCalledTimes(2)
 | 
					    expect(reloadSpy).toHaveBeenCalledTimes(2)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should retry a task, show toast on error or success', () => {
 | 
				
			||||||
 | 
					    const retrySpy = jest.spyOn(tasksService, 'retryTask')
 | 
				
			||||||
 | 
					    const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
 | 
				
			||||||
 | 
					    const toastErrorSpy = jest.spyOn(toastService, 'showError')
 | 
				
			||||||
 | 
					    retrySpy.mockReturnValueOnce(of({ task_id: '123' }))
 | 
				
			||||||
 | 
					    component.retryTask(tasks[0])
 | 
				
			||||||
 | 
					    expect(retrySpy).toHaveBeenCalledWith(tasks[0], false)
 | 
				
			||||||
 | 
					    expect(toastInfoSpy).toHaveBeenCalledWith('Retrying task...')
 | 
				
			||||||
 | 
					    retrySpy.mockReturnValueOnce(throwError(() => new Error('test')))
 | 
				
			||||||
 | 
					    component.retryTask(tasks[0])
 | 
				
			||||||
 | 
					    expect(toastErrorSpy).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					      'Failed to retry task',
 | 
				
			||||||
 | 
					      new Error('test')
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,11 @@ import { Component, OnInit, OnDestroy } from '@angular/core'
 | 
				
			|||||||
import { Router } from '@angular/router'
 | 
					import { Router } from '@angular/router'
 | 
				
			||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
					import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
				
			||||||
import { first } from 'rxjs'
 | 
					import { first } from 'rxjs'
 | 
				
			||||||
import { PaperlessTask } from 'src/app/data/paperless-task'
 | 
					import { PaperlessTask, PaperlessTaskStatus } from 'src/app/data/paperless-task'
 | 
				
			||||||
import { TasksService } from 'src/app/services/tasks.service'
 | 
					import { TasksService } from 'src/app/services/tasks.service'
 | 
				
			||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
 | 
					import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
 | 
				
			||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
 | 
					import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
 | 
				
			||||||
 | 
					import { ToastService } from 'src/app/services/toast.service'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'pngx-tasks',
 | 
					  selector: 'pngx-tasks',
 | 
				
			||||||
@@ -16,6 +17,7 @@ export class TasksComponent
 | 
				
			|||||||
  extends ComponentWithPermissions
 | 
					  extends ComponentWithPermissions
 | 
				
			||||||
  implements OnInit, OnDestroy
 | 
					  implements OnInit, OnDestroy
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					  public PaperlessTaskStatus = PaperlessTaskStatus
 | 
				
			||||||
  public activeTab: string
 | 
					  public activeTab: string
 | 
				
			||||||
  public selectedTasks: Set<number> = new Set()
 | 
					  public selectedTasks: Set<number> = new Set()
 | 
				
			||||||
  public togggleAll: boolean = false
 | 
					  public togggleAll: boolean = false
 | 
				
			||||||
@@ -26,6 +28,8 @@ export class TasksComponent
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public autoRefreshInterval: any
 | 
					  public autoRefreshInterval: any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public retryClean: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get dismissButtonText(): string {
 | 
					  get dismissButtonText(): string {
 | 
				
			||||||
    return this.selectedTasks.size > 0
 | 
					    return this.selectedTasks.size > 0
 | 
				
			||||||
      ? $localize`Dismiss selected`
 | 
					      ? $localize`Dismiss selected`
 | 
				
			||||||
@@ -35,6 +39,7 @@ export class TasksComponent
 | 
				
			|||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public tasksService: TasksService,
 | 
					    public tasksService: TasksService,
 | 
				
			||||||
    private modalService: NgbModal,
 | 
					    private modalService: NgbModal,
 | 
				
			||||||
 | 
					    private toastService: ToastService,
 | 
				
			||||||
    private readonly router: Router
 | 
					    private readonly router: Router
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
@@ -83,6 +88,17 @@ export class TasksComponent
 | 
				
			|||||||
    this.router.navigate(['documents', task.related_document])
 | 
					    this.router.navigate(['documents', task.related_document])
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  retryTask(task: PaperlessTask) {
 | 
				
			||||||
 | 
					    this.tasksService.retryTask(task, this.retryClean).subscribe({
 | 
				
			||||||
 | 
					      next: () => {
 | 
				
			||||||
 | 
					        this.toastService.showInfo($localize`Retrying task...`)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      error: (e) => {
 | 
				
			||||||
 | 
					        this.toastService.showError($localize`Failed to retry task`, e)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  expandTask(task: PaperlessTask) {
 | 
					  expandTask(task: PaperlessTask) {
 | 
				
			||||||
    this.expandedTask = this.expandedTask == task.id ? undefined : task.id
 | 
					    this.expandedTask = this.expandedTask == task.id ? undefined : task.id
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,4 +118,29 @@ describe('TasksService', () => {
 | 
				
			|||||||
    expect(tasksService.queuedFileTasks).toHaveLength(1)
 | 
					    expect(tasksService.queuedFileTasks).toHaveLength(1)
 | 
				
			||||||
    expect(tasksService.startedFileTasks).toHaveLength(1)
 | 
					    expect(tasksService.startedFileTasks).toHaveLength(1)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should call retry task api endpoint', () => {
 | 
				
			||||||
 | 
					    const task = {
 | 
				
			||||||
 | 
					      id: 1,
 | 
				
			||||||
 | 
					      type: PaperlessTaskType.File,
 | 
				
			||||||
 | 
					      status: PaperlessTaskStatus.Failed,
 | 
				
			||||||
 | 
					      acknowledged: false,
 | 
				
			||||||
 | 
					      task_id: '1234',
 | 
				
			||||||
 | 
					      task_file_name: 'file1.pdf',
 | 
				
			||||||
 | 
					      date_created: new Date(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tasksService.retryTask(task, true).subscribe()
 | 
				
			||||||
 | 
					    const reloadSpy = jest.spyOn(tasksService, 'reload')
 | 
				
			||||||
 | 
					    const req = httpTestingController.expectOne(
 | 
				
			||||||
 | 
					      `${environment.apiBaseUrl}tasks/${task.id}/retry/`
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expect(req.request.method).toEqual('POST')
 | 
				
			||||||
 | 
					    expect(req.request.body).toEqual({
 | 
				
			||||||
 | 
					      clean: true,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    req.flush({ task_id: 12345 })
 | 
				
			||||||
 | 
					    expect(reloadSpy).toHaveBeenCalled()
 | 
				
			||||||
 | 
					    httpTestingController.expectOne(`${environment.apiBaseUrl}tasks/`).flush([])
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import { HttpClient } from '@angular/common/http'
 | 
					import { HttpClient } from '@angular/common/http'
 | 
				
			||||||
import { Injectable } from '@angular/core'
 | 
					import { Injectable } from '@angular/core'
 | 
				
			||||||
import { Subject } from 'rxjs'
 | 
					import { Observable, Subject } from 'rxjs'
 | 
				
			||||||
import { first, takeUntil } from 'rxjs/operators'
 | 
					import { first, takeUntil, tap } from 'rxjs/operators'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  PaperlessTask,
 | 
					  PaperlessTask,
 | 
				
			||||||
  PaperlessTaskStatus,
 | 
					  PaperlessTaskStatus,
 | 
				
			||||||
@@ -73,6 +73,20 @@ export class TasksService {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public retryTask(task: PaperlessTask, clean: boolean): Observable<any> {
 | 
				
			||||||
 | 
					    return this.http
 | 
				
			||||||
 | 
					      .post(`${this.baseUrl}tasks/${task.id}/retry/`, {
 | 
				
			||||||
 | 
					        clean,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        takeUntil(this.unsubscribeNotifer),
 | 
				
			||||||
 | 
					        first(),
 | 
				
			||||||
 | 
					        tap(() => {
 | 
				
			||||||
 | 
					          this.reload()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public cancelPending(): void {
 | 
					  public cancelPending(): void {
 | 
				
			||||||
    this.unsubscribeNotifer.next(true)
 | 
					    this.unsubscribeNotifer.next(true)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -148,6 +148,17 @@ class ConsumerPlugin(
 | 
				
			|||||||
    ):
 | 
					    ):
 | 
				
			||||||
        self._send_progress(100, 100, ProgressStatusOptions.FAILED, message)
 | 
					        self._send_progress(100, 100, ProgressStatusOptions.FAILED, message)
 | 
				
			||||||
        self.log.error(log_message or message, exc_info=exc_info)
 | 
					        self.log.error(log_message or message, exc_info=exc_info)
 | 
				
			||||||
 | 
					        # Move the file to the failed directory
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					            self.input_doc.original_file.exists()
 | 
				
			||||||
 | 
					            and not Path(
 | 
				
			||||||
 | 
					                settings.CONSUMPTION_FAILED_DIR / self.input_doc.original_file.name,
 | 
				
			||||||
 | 
					            ).exists()
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            copy_file_with_basic_stats(
 | 
				
			||||||
 | 
					                self.input_doc.original_file,
 | 
				
			||||||
 | 
					                settings.CONSUMPTION_FAILED_DIR / self.input_doc.original_file.name,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        raise ConsumerError(f"{self.filename}: {log_message or message}") from exception
 | 
					        raise ConsumerError(f"{self.filename}: {log_message or message}") from exception
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pre_check_file_exists(self):
 | 
					    def pre_check_file_exists(self):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -679,24 +679,28 @@ class PaperlessTask(models.Model):
 | 
				
			|||||||
        verbose_name=_("Task State"),
 | 
					        verbose_name=_("Task State"),
 | 
				
			||||||
        help_text=_("Current state of the task being run"),
 | 
					        help_text=_("Current state of the task being run"),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    date_created = models.DateTimeField(
 | 
					    date_created = models.DateTimeField(
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        default=timezone.now,
 | 
					        default=timezone.now,
 | 
				
			||||||
        verbose_name=_("Created DateTime"),
 | 
					        verbose_name=_("Created DateTime"),
 | 
				
			||||||
        help_text=_("Datetime field when the task result was created in UTC"),
 | 
					        help_text=_("Datetime field when the task result was created in UTC"),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    date_started = models.DateTimeField(
 | 
					    date_started = models.DateTimeField(
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        default=None,
 | 
					        default=None,
 | 
				
			||||||
        verbose_name=_("Started DateTime"),
 | 
					        verbose_name=_("Started DateTime"),
 | 
				
			||||||
        help_text=_("Datetime field when the task was started in UTC"),
 | 
					        help_text=_("Datetime field when the task was started in UTC"),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    date_done = models.DateTimeField(
 | 
					    date_done = models.DateTimeField(
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        default=None,
 | 
					        default=None,
 | 
				
			||||||
        verbose_name=_("Completed DateTime"),
 | 
					        verbose_name=_("Completed DateTime"),
 | 
				
			||||||
        help_text=_("Datetime field when the task was completed in UTC"),
 | 
					        help_text=_("Datetime field when the task was completed in UTC"),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result = models.TextField(
 | 
					    result = models.TextField(
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        default=None,
 | 
					        default=None,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1585,6 +1585,14 @@ class TasksViewSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
        return result
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RetryTaskSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					    clean = serializers.BooleanField(
 | 
				
			||||||
 | 
					        default=False,
 | 
				
			||||||
 | 
					        write_only=True,
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AcknowledgeTasksViewSerializer(serializers.Serializer):
 | 
					class AcknowledgeTasksViewSerializer(serializers.Serializer):
 | 
				
			||||||
    tasks = serializers.ListField(
 | 
					    tasks = serializers.ListField(
 | 
				
			||||||
        required=True,
 | 
					        required=True,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from celery import states
 | 
					from celery import states
 | 
				
			||||||
from celery.signals import before_task_publish
 | 
					from celery.signals import before_task_publish
 | 
				
			||||||
@@ -520,6 +521,19 @@ def update_filename_and_move_files(
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@receiver(models.signals.post_save, sender=PaperlessTask)
 | 
				
			||||||
 | 
					def cleanup_failed_documents(sender, instance: PaperlessTask, **kwargs):
 | 
				
			||||||
 | 
					    if instance.status != states.FAILURE or not instance.acknowledged:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if instance.task_file_name:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            Path(settings.CONSUMPTION_FAILED_DIR / instance.task_file_name).unlink()
 | 
				
			||||||
 | 
					            logger.debug(f"Cleaned up failed file {instance.task_file_name}")
 | 
				
			||||||
 | 
					        except FileNotFoundError:
 | 
				
			||||||
 | 
					            logger.warning(f"Failed to clean up failed file {instance.task_file_name}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def set_log_entry(sender, document: Document, logging_group=None, **kwargs):
 | 
					def set_log_entry(sender, document: Document, logging_group=None, **kwargs):
 | 
				
			||||||
    ct = ContentType.objects.get(model="document")
 | 
					    ct = ContentType.objects.get(model="document")
 | 
				
			||||||
    user = User.objects.get(username="consumer")
 | 
					    user = User.objects.get(username="consumer")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ from tempfile import TemporaryDirectory
 | 
				
			|||||||
import tqdm
 | 
					import tqdm
 | 
				
			||||||
from celery import Task
 | 
					from celery import Task
 | 
				
			||||||
from celery import shared_task
 | 
					from celery import shared_task
 | 
				
			||||||
 | 
					from celery import states
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db import transaction
 | 
					from django.db import transaction
 | 
				
			||||||
@@ -27,12 +28,14 @@ from documents.consumer import ConsumerPlugin
 | 
				
			|||||||
from documents.consumer import WorkflowTriggerPlugin
 | 
					from documents.consumer import WorkflowTriggerPlugin
 | 
				
			||||||
from documents.data_models import ConsumableDocument
 | 
					from documents.data_models import ConsumableDocument
 | 
				
			||||||
from documents.data_models import DocumentMetadataOverrides
 | 
					from documents.data_models import DocumentMetadataOverrides
 | 
				
			||||||
 | 
					from documents.data_models import DocumentSource
 | 
				
			||||||
from documents.double_sided import CollatePlugin
 | 
					from documents.double_sided import CollatePlugin
 | 
				
			||||||
from documents.file_handling import create_source_path_directory
 | 
					from documents.file_handling import create_source_path_directory
 | 
				
			||||||
from documents.file_handling import generate_unique_filename
 | 
					from documents.file_handling import generate_unique_filename
 | 
				
			||||||
from documents.models import Correspondent
 | 
					from documents.models import Correspondent
 | 
				
			||||||
from documents.models import Document
 | 
					from documents.models import Document
 | 
				
			||||||
from documents.models import DocumentType
 | 
					from documents.models import DocumentType
 | 
				
			||||||
 | 
					from documents.models import PaperlessTask
 | 
				
			||||||
from documents.models import StoragePath
 | 
					from documents.models import StoragePath
 | 
				
			||||||
from documents.models import Tag
 | 
					from documents.models import Tag
 | 
				
			||||||
from documents.parsers import DocumentParser
 | 
					from documents.parsers import DocumentParser
 | 
				
			||||||
@@ -44,6 +47,8 @@ from documents.plugins.helpers import ProgressStatusOptions
 | 
				
			|||||||
from documents.sanity_checker import SanityCheckFailedException
 | 
					from documents.sanity_checker import SanityCheckFailedException
 | 
				
			||||||
from documents.signals import document_updated
 | 
					from documents.signals import document_updated
 | 
				
			||||||
from documents.signals.handlers import cleanup_document_deletion
 | 
					from documents.signals.handlers import cleanup_document_deletion
 | 
				
			||||||
 | 
					from documents.utils import copy_file_with_basic_stats
 | 
				
			||||||
 | 
					from documents.utils import run_subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if settings.AUDIT_LOG_ENABLED:
 | 
					if settings.AUDIT_LOG_ENABLED:
 | 
				
			||||||
    from auditlog.models import LogEntry
 | 
					    from auditlog.models import LogEntry
 | 
				
			||||||
@@ -169,6 +174,48 @@ def consume_file(
 | 
				
			|||||||
    return msg
 | 
					    return msg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@shared_task
 | 
				
			||||||
 | 
					def retry_failed_file(task_id: str, clean: bool = False, skip_ocr: bool = False):
 | 
				
			||||||
 | 
					    task = PaperlessTask.objects.get(task_id=task_id, status=states.FAILURE)
 | 
				
			||||||
 | 
					    if task:
 | 
				
			||||||
 | 
					        failed_file = settings.CONSUMPTION_FAILED_DIR / task.task_file_name
 | 
				
			||||||
 | 
					        if not failed_file.exists():
 | 
				
			||||||
 | 
					            logger.error(f"File {failed_file} not found")
 | 
				
			||||||
 | 
					            raise FileNotFoundError(f"File {failed_file} not found")
 | 
				
			||||||
 | 
					        working_copy = settings.SCRATCH_DIR / failed_file.name
 | 
				
			||||||
 | 
					        copy_file_with_basic_stats(failed_file, working_copy)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if clean:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                result = run_subprocess(
 | 
				
			||||||
 | 
					                    [
 | 
				
			||||||
 | 
					                        "qpdf",
 | 
				
			||||||
 | 
					                        "--replace-input",
 | 
				
			||||||
 | 
					                        "--warning-exit-0",
 | 
				
			||||||
 | 
					                        working_copy,
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    logger=logger,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                if result.returncode != 0:
 | 
				
			||||||
 | 
					                    raise Exception(
 | 
				
			||||||
 | 
					                        f"qpdf failed with exit code {result.returncode}, error: {result.stderr}",
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    logger.debug("PDF cleaned successfully")
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                logger.error(f"Error while cleaning PDF: {e}")
 | 
				
			||||||
 | 
					                raise e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        task = consume_file.delay(
 | 
				
			||||||
 | 
					            ConsumableDocument(
 | 
				
			||||||
 | 
					                source=DocumentSource.ConsumeFolder,
 | 
				
			||||||
 | 
					                original_file=working_copy,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return task.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@shared_task
 | 
					@shared_task
 | 
				
			||||||
def sanity_check():
 | 
					def sanity_check():
 | 
				
			||||||
    messages = sanity_checker.check_sanity()
 | 
					    messages = sanity_checker.check_sanity()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/corrupted.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/documents/tests/samples/corrupted.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,5 +1,8 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
from datetime import timedelta
 | 
					from datetime import timedelta
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
from unittest import mock
 | 
					from unittest import mock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
@@ -7,15 +10,22 @@ from django.test import TestCase
 | 
				
			|||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from documents import tasks
 | 
					from documents import tasks
 | 
				
			||||||
 | 
					from documents.data_models import ConsumableDocument
 | 
				
			||||||
 | 
					from documents.data_models import DocumentSource
 | 
				
			||||||
from documents.models import Correspondent
 | 
					from documents.models import Correspondent
 | 
				
			||||||
from documents.models import Document
 | 
					from documents.models import Document
 | 
				
			||||||
from documents.models import DocumentType
 | 
					from documents.models import DocumentType
 | 
				
			||||||
 | 
					from documents.models import PaperlessTask
 | 
				
			||||||
from documents.models import Tag
 | 
					from documents.models import Tag
 | 
				
			||||||
from documents.sanity_checker import SanityCheckFailedException
 | 
					from documents.sanity_checker import SanityCheckFailedException
 | 
				
			||||||
from documents.sanity_checker import SanityCheckMessages
 | 
					from documents.sanity_checker import SanityCheckMessages
 | 
				
			||||||
 | 
					from documents.signals.handlers import before_task_publish_handler
 | 
				
			||||||
 | 
					from documents.signals.handlers import task_failure_handler
 | 
				
			||||||
from documents.tests.test_classifier import dummy_preprocess
 | 
					from documents.tests.test_classifier import dummy_preprocess
 | 
				
			||||||
from documents.tests.utils import DirectoriesMixin
 | 
					from documents.tests.utils import DirectoriesMixin
 | 
				
			||||||
 | 
					from documents.tests.utils import DummyProgressManager
 | 
				
			||||||
from documents.tests.utils import FileSystemAssertsMixin
 | 
					from documents.tests.utils import FileSystemAssertsMixin
 | 
				
			||||||
 | 
					from documents.tests.utils import SampleDirMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestIndexReindex(DirectoriesMixin, TestCase):
 | 
					class TestIndexReindex(DirectoriesMixin, TestCase):
 | 
				
			||||||
@@ -184,3 +194,68 @@ class TestEmptyTrashTask(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        tasks.empty_trash()
 | 
					        tasks.empty_trash()
 | 
				
			||||||
        self.assertEqual(Document.global_objects.count(), 0)
 | 
					        self.assertEqual(Document.global_objects.count(), 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestRetryConsumeTask(
 | 
				
			||||||
 | 
					    DirectoriesMixin,
 | 
				
			||||||
 | 
					    SampleDirMixin,
 | 
				
			||||||
 | 
					    FileSystemAssertsMixin,
 | 
				
			||||||
 | 
					    TestCase,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    def do_failed_task(self, test_file: Path) -> PaperlessTask:
 | 
				
			||||||
 | 
					        temp_copy = self.dirs.scratch_dir / test_file.name
 | 
				
			||||||
 | 
					        shutil.copy(test_file, temp_copy)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        headers = {
 | 
				
			||||||
 | 
					            "id": str(uuid.uuid4()),
 | 
				
			||||||
 | 
					            "task": "documents.tasks.consume_file",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        body = (
 | 
				
			||||||
 | 
					            # args
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                ConsumableDocument(
 | 
				
			||||||
 | 
					                    source=DocumentSource.ConsumeFolder,
 | 
				
			||||||
 | 
					                    original_file=str(temp_copy),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                None,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            # kwargs
 | 
				
			||||||
 | 
					            {},
 | 
				
			||||||
 | 
					            # celery stuff
 | 
				
			||||||
 | 
					            {"callbacks": None, "errbacks": None, "chain": None, "chord": None},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        before_task_publish_handler(headers=headers, body=body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
 | 
				
			||||||
 | 
					            with self.assertRaises(Exception):
 | 
				
			||||||
 | 
					                tasks.consume_file(
 | 
				
			||||||
 | 
					                    ConsumableDocument(
 | 
				
			||||||
 | 
					                        source=DocumentSource.ConsumeFolder,
 | 
				
			||||||
 | 
					                        original_file=temp_copy,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        task_failure_handler(
 | 
				
			||||||
 | 
					            task_id=headers["id"],
 | 
				
			||||||
 | 
					            exception="Example failure",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        task = PaperlessTask.objects.first()
 | 
				
			||||||
 | 
					        # Ensure the file is moved to the failed dir
 | 
				
			||||||
 | 
					        self.assertIsFile(settings.CONSUMPTION_FAILED_DIR / task.task_file_name)
 | 
				
			||||||
 | 
					        return task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @mock.patch("documents.tasks.consume_file.delay")
 | 
				
			||||||
 | 
					    @mock.patch("documents.tasks.run_subprocess")
 | 
				
			||||||
 | 
					    def test_retry_consume_clean(self, m_subprocess, m_consume_file):
 | 
				
			||||||
 | 
					        task = self.do_failed_task(self.SAMPLE_DIR / "corrupted.pdf")
 | 
				
			||||||
 | 
					        m_subprocess.return_value.returncode = 0
 | 
				
			||||||
 | 
					        task_id = tasks.retry_failed_file(task_id=task.task_id, clean=True)
 | 
				
			||||||
 | 
					        self.assertIsNotNone(task_id)
 | 
				
			||||||
 | 
					        m_consume_file.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_cleanup(self):
 | 
				
			||||||
 | 
					        task = self.do_failed_task(self.SAMPLE_DIR / "corrupted.pdf")
 | 
				
			||||||
 | 
					        task.acknowledged = True
 | 
				
			||||||
 | 
					        task.save()  # simulate the task being acknowledged
 | 
				
			||||||
 | 
					        self.assertIsNotFile(settings.CONSUMPTION_FAILED_DIR / task.task_file_name)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,6 +35,7 @@ def setup_directories():
 | 
				
			|||||||
    dirs.scratch_dir = Path(tempfile.mkdtemp())
 | 
					    dirs.scratch_dir = Path(tempfile.mkdtemp())
 | 
				
			||||||
    dirs.media_dir = Path(tempfile.mkdtemp())
 | 
					    dirs.media_dir = Path(tempfile.mkdtemp())
 | 
				
			||||||
    dirs.consumption_dir = Path(tempfile.mkdtemp())
 | 
					    dirs.consumption_dir = Path(tempfile.mkdtemp())
 | 
				
			||||||
 | 
					    dirs.consumption_failed_dir = Path(tempfile.mkdtemp("failed"))
 | 
				
			||||||
    dirs.static_dir = Path(tempfile.mkdtemp())
 | 
					    dirs.static_dir = Path(tempfile.mkdtemp())
 | 
				
			||||||
    dirs.index_dir = dirs.data_dir / "index"
 | 
					    dirs.index_dir = dirs.data_dir / "index"
 | 
				
			||||||
    dirs.originals_dir = dirs.media_dir / "documents" / "originals"
 | 
					    dirs.originals_dir = dirs.media_dir / "documents" / "originals"
 | 
				
			||||||
@@ -56,6 +57,7 @@ def setup_directories():
 | 
				
			|||||||
        THUMBNAIL_DIR=dirs.thumbnail_dir,
 | 
					        THUMBNAIL_DIR=dirs.thumbnail_dir,
 | 
				
			||||||
        ARCHIVE_DIR=dirs.archive_dir,
 | 
					        ARCHIVE_DIR=dirs.archive_dir,
 | 
				
			||||||
        CONSUMPTION_DIR=dirs.consumption_dir,
 | 
					        CONSUMPTION_DIR=dirs.consumption_dir,
 | 
				
			||||||
 | 
					        CONSUMPTION_FAILED_DIR=dirs.consumption_failed_dir,
 | 
				
			||||||
        LOGGING_DIR=dirs.logging_dir,
 | 
					        LOGGING_DIR=dirs.logging_dir,
 | 
				
			||||||
        INDEX_DIR=dirs.index_dir,
 | 
					        INDEX_DIR=dirs.index_dir,
 | 
				
			||||||
        STATIC_ROOT=dirs.static_dir,
 | 
					        STATIC_ROOT=dirs.static_dir,
 | 
				
			||||||
@@ -72,6 +74,7 @@ def remove_dirs(dirs):
 | 
				
			|||||||
    shutil.rmtree(dirs.data_dir, ignore_errors=True)
 | 
					    shutil.rmtree(dirs.data_dir, ignore_errors=True)
 | 
				
			||||||
    shutil.rmtree(dirs.scratch_dir, ignore_errors=True)
 | 
					    shutil.rmtree(dirs.scratch_dir, ignore_errors=True)
 | 
				
			||||||
    shutil.rmtree(dirs.consumption_dir, ignore_errors=True)
 | 
					    shutil.rmtree(dirs.consumption_dir, ignore_errors=True)
 | 
				
			||||||
 | 
					    shutil.rmtree(dirs.consumption_failed_dir, ignore_errors=True)
 | 
				
			||||||
    shutil.rmtree(dirs.static_dir, ignore_errors=True)
 | 
					    shutil.rmtree(dirs.static_dir, ignore_errors=True)
 | 
				
			||||||
    dirs.settings_override.disable()
 | 
					    dirs.settings_override.disable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,6 +136,7 @@ from documents.serialisers import DocumentListSerializer
 | 
				
			|||||||
from documents.serialisers import DocumentSerializer
 | 
					from documents.serialisers import DocumentSerializer
 | 
				
			||||||
from documents.serialisers import DocumentTypeSerializer
 | 
					from documents.serialisers import DocumentTypeSerializer
 | 
				
			||||||
from documents.serialisers import PostDocumentSerializer
 | 
					from documents.serialisers import PostDocumentSerializer
 | 
				
			||||||
 | 
					from documents.serialisers import RetryTaskSerializer
 | 
				
			||||||
from documents.serialisers import SavedViewSerializer
 | 
					from documents.serialisers import SavedViewSerializer
 | 
				
			||||||
from documents.serialisers import SearchResultSerializer
 | 
					from documents.serialisers import SearchResultSerializer
 | 
				
			||||||
from documents.serialisers import ShareLinkSerializer
 | 
					from documents.serialisers import ShareLinkSerializer
 | 
				
			||||||
@@ -152,6 +153,7 @@ from documents.serialisers import WorkflowTriggerSerializer
 | 
				
			|||||||
from documents.signals import document_updated
 | 
					from documents.signals import document_updated
 | 
				
			||||||
from documents.tasks import consume_file
 | 
					from documents.tasks import consume_file
 | 
				
			||||||
from documents.tasks import empty_trash
 | 
					from documents.tasks import empty_trash
 | 
				
			||||||
 | 
					from documents.tasks import retry_failed_file
 | 
				
			||||||
from documents.templating.filepath import validate_filepath_template_and_render
 | 
					from documents.templating.filepath import validate_filepath_template_and_render
 | 
				
			||||||
from paperless import version
 | 
					from paperless import version
 | 
				
			||||||
from paperless.celery import app as celery_app
 | 
					from paperless.celery import app as celery_app
 | 
				
			||||||
@@ -1718,6 +1720,25 @@ class TasksViewSet(ReadOnlyModelViewSet):
 | 
				
			|||||||
            queryset = PaperlessTask.objects.filter(task_id=task_id)
 | 
					            queryset = PaperlessTask.objects.filter(task_id=task_id)
 | 
				
			||||||
        return queryset
 | 
					        return queryset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @action(methods=["post"], detail=True)
 | 
				
			||||||
 | 
					    def retry(self, request, pk=None):
 | 
				
			||||||
 | 
					        task = self.get_object()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = RetryTaskSerializer(data=request.data)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        clean = serializer.validated_data.get("clean")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            new_task_id = retry_failed_file(task.task_id, clean)
 | 
				
			||||||
 | 
					            return Response({"task_id": new_task_id})
 | 
				
			||||||
 | 
					        except FileNotFoundError:
 | 
				
			||||||
 | 
					            return HttpResponseBadRequest("Original file not found")
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.warning(f"An error occurred retrying task: {e!s}")
 | 
				
			||||||
 | 
					            return HttpResponseBadRequest(
 | 
				
			||||||
 | 
					                "Error retrying task, check logs for more detail.",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AcknowledgeTasksView(GenericAPIView):
 | 
					class AcknowledgeTasksView(GenericAPIView):
 | 
				
			||||||
    permission_classes = (IsAuthenticated,)
 | 
					    permission_classes = (IsAuthenticated,)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,10 @@ def paths_check(app_configs, **kwargs):
 | 
				
			|||||||
        + path_check("PAPERLESS_EMPTY_TRASH_DIR", settings.EMPTY_TRASH_DIR)
 | 
					        + path_check("PAPERLESS_EMPTY_TRASH_DIR", settings.EMPTY_TRASH_DIR)
 | 
				
			||||||
        + path_check("PAPERLESS_MEDIA_ROOT", settings.MEDIA_ROOT)
 | 
					        + path_check("PAPERLESS_MEDIA_ROOT", settings.MEDIA_ROOT)
 | 
				
			||||||
        + path_check("PAPERLESS_CONSUMPTION_DIR", settings.CONSUMPTION_DIR)
 | 
					        + path_check("PAPERLESS_CONSUMPTION_DIR", settings.CONSUMPTION_DIR)
 | 
				
			||||||
 | 
					        + path_check(
 | 
				
			||||||
 | 
					            "PAPERLESS_CONSUMPTION_FAILED_DIR",
 | 
				
			||||||
 | 
					            settings.CONSUMPTION_FAILED_DIR,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -281,6 +281,11 @@ CONSUMPTION_DIR = __get_path(
 | 
				
			|||||||
    BASE_DIR.parent / "consume",
 | 
					    BASE_DIR.parent / "consume",
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONSUMPTION_FAILED_DIR = __get_path(
 | 
				
			||||||
 | 
					    "PAPERLESS_CONSUMPTION_FAILED_DIR",
 | 
				
			||||||
 | 
					    CONSUMPTION_DIR / "failed",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This will be created if it doesn't exist
 | 
					# This will be created if it doesn't exist
 | 
				
			||||||
SCRATCH_DIR = __get_path(
 | 
					SCRATCH_DIR = __get_path(
 | 
				
			||||||
    "PAPERLESS_SCRATCH_DIR",
 | 
					    "PAPERLESS_SCRATCH_DIR",
 | 
				
			||||||
@@ -890,6 +895,8 @@ CONSUMER_IGNORE_PATTERNS = list(
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					if CONSUMPTION_DIR in CONSUMPTION_FAILED_DIR.parents:
 | 
				
			||||||
 | 
					    CONSUMER_IGNORE_PATTERNS.append(CONSUMPTION_FAILED_DIR.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONSUMER_SUBDIRS_AS_TAGS = __get_boolean("PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS")
 | 
					CONSUMER_SUBDIRS_AS_TAGS = __get_boolean("PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,10 +29,11 @@ class TestChecks(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        MEDIA_ROOT="uuh",
 | 
					        MEDIA_ROOT="uuh",
 | 
				
			||||||
        DATA_DIR="whatever",
 | 
					        DATA_DIR="whatever",
 | 
				
			||||||
        CONSUMPTION_DIR="idontcare",
 | 
					        CONSUMPTION_DIR="idontcare",
 | 
				
			||||||
 | 
					        CONSUMPTION_FAILED_DIR="nope",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_paths_check_dont_exist(self):
 | 
					    def test_paths_check_dont_exist(self):
 | 
				
			||||||
        msgs = paths_check(None)
 | 
					        msgs = paths_check(None)
 | 
				
			||||||
        self.assertEqual(len(msgs), 3, str(msgs))
 | 
					        self.assertEqual(len(msgs), 4, str(msgs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for msg in msgs:
 | 
					        for msg in msgs:
 | 
				
			||||||
            self.assertTrue(msg.msg.endswith("is set but doesn't exist."))
 | 
					            self.assertTrue(msg.msg.endswith("is set but doesn't exist."))
 | 
				
			||||||
@@ -41,13 +42,15 @@ class TestChecks(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        os.chmod(self.dirs.data_dir, 0o000)
 | 
					        os.chmod(self.dirs.data_dir, 0o000)
 | 
				
			||||||
        os.chmod(self.dirs.media_dir, 0o000)
 | 
					        os.chmod(self.dirs.media_dir, 0o000)
 | 
				
			||||||
        os.chmod(self.dirs.consumption_dir, 0o000)
 | 
					        os.chmod(self.dirs.consumption_dir, 0o000)
 | 
				
			||||||
 | 
					        os.chmod(self.dirs.consumption_failed_dir, 0o000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.addCleanup(os.chmod, self.dirs.data_dir, 0o777)
 | 
					        self.addCleanup(os.chmod, self.dirs.data_dir, 0o777)
 | 
				
			||||||
        self.addCleanup(os.chmod, self.dirs.media_dir, 0o777)
 | 
					        self.addCleanup(os.chmod, self.dirs.media_dir, 0o777)
 | 
				
			||||||
        self.addCleanup(os.chmod, self.dirs.consumption_dir, 0o777)
 | 
					        self.addCleanup(os.chmod, self.dirs.consumption_dir, 0o777)
 | 
				
			||||||
 | 
					        self.addCleanup(os.chmod, self.dirs.consumption_failed_dir, 0o777)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        msgs = paths_check(None)
 | 
					        msgs = paths_check(None)
 | 
				
			||||||
        self.assertEqual(len(msgs), 3)
 | 
					        self.assertEqual(len(msgs), 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for msg in msgs:
 | 
					        for msg in msgs:
 | 
				
			||||||
            self.assertTrue(msg.msg.endswith("is not writeable"))
 | 
					            self.assertTrue(msg.msg.endswith("is not writeable"))
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user