mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-25 10:49:30 -05:00
Enhancement: system status report sanity check, simpler classifier check, styling updates (#9106)
This commit is contained in:
parent
ec34197b59
commit
2d52226732
@ -1514,7 +1514,7 @@
|
|||||||
<source>Error retrieving users</source>
|
<source>Error retrieving users</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">217</context>
|
<context context-type="linenumber">220</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
||||||
@ -1525,7 +1525,7 @@
|
|||||||
<source>Error retrieving groups</source>
|
<source>Error retrieving groups</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">236</context>
|
<context context-type="linenumber">239</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
|
||||||
@ -1536,28 +1536,28 @@
|
|||||||
<source>Settings were saved successfully.</source>
|
<source>Settings were saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">532</context>
|
<context context-type="linenumber">535</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="525012668859298131" datatype="html">
|
<trans-unit id="525012668859298131" datatype="html">
|
||||||
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
|
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">536</context>
|
<context context-type="linenumber">539</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8491974984518503778" datatype="html">
|
<trans-unit id="8491974984518503778" datatype="html">
|
||||||
<source>Reload now</source>
|
<source>Reload now</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">537</context>
|
<context context-type="linenumber">540</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3011185103048412841" datatype="html">
|
<trans-unit id="3011185103048412841" datatype="html">
|
||||||
<source>An error occurred while saving settings.</source>
|
<source>An error occurred while saving settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">547</context>
|
<context context-type="linenumber">550</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
@ -4099,6 +4099,18 @@
|
|||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
||||||
<context context-type="linenumber">111</context>
|
<context context-type="linenumber">111</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">175</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">209</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">243</context>
|
||||||
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/toast/toast.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/toast/toast.component.html</context>
|
||||||
<context context-type="linenumber">30</context>
|
<context context-type="linenumber">30</context>
|
||||||
@ -5615,7 +5627,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">156</context>
|
<context context-type="linenumber">261</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
|
||||||
@ -6010,77 +6022,113 @@
|
|||||||
<source>Migration Status</source>
|
<source>Migration Status</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">56</context>
|
<context context-type="linenumber">65</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7489316373554112115" datatype="html">
|
<trans-unit id="7489316373554112115" datatype="html">
|
||||||
<source>Up to date</source>
|
<source>Up to date</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">59</context>
|
<context context-type="linenumber">69</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7881311375431899727" datatype="html">
|
<trans-unit id="7881311375431899727" datatype="html">
|
||||||
<source>Latest Migration</source>
|
<source>Latest Migration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">64</context>
|
<context context-type="linenumber">74</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4632965004151576238" datatype="html">
|
<trans-unit id="4632965004151576238" datatype="html">
|
||||||
<source>Pending Migrations</source>
|
<source>Pending Migrations</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">66</context>
|
<context context-type="linenumber">76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6904866445262015585" datatype="html">
|
<trans-unit id="2790343143501919450" datatype="html">
|
||||||
<source>Tasks</source>
|
<source>Tasks Queue</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">83</context>
|
<context context-type="linenumber">94</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6911698235105017958" datatype="html">
|
<trans-unit id="6911698235105017958" datatype="html">
|
||||||
<source>Redis Status</source>
|
<source>Redis Status</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">98</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5349496739889768589" datatype="html">
|
<trans-unit id="5349496739889768589" datatype="html">
|
||||||
<source>Celery Status</source>
|
<source>Celery Status</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">96</context>
|
<context context-type="linenumber">116</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2041675390931385838" datatype="html">
|
||||||
|
<source>Health</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">142</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="31377277941774469" datatype="html">
|
<trans-unit id="31377277941774469" datatype="html">
|
||||||
<source>Search Index</source>
|
<source>Search Index</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">146</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9127131074422113272" datatype="html">
|
||||||
|
<source>Run Task</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">166</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">200</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">234</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4089509911694721896" datatype="html">
|
<trans-unit id="4089509911694721896" datatype="html">
|
||||||
<source>Last Updated</source>
|
<source>Last Updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">173</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="46628344485199198" datatype="html">
|
<trans-unit id="46628344485199198" datatype="html">
|
||||||
<source>Classifier</source>
|
<source>Classifier</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">121</context>
|
<context context-type="linenumber">178</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6096684179126491743" datatype="html">
|
<trans-unit id="6096684179126491743" datatype="html">
|
||||||
<source>Last Trained</source>
|
<source>Last Trained</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
<context context-type="linenumber">139</context>
|
<context context-type="linenumber">207</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6427836860962380759" datatype="html">
|
||||||
|
<source>Sanity Checker</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">212</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6578747070254776938" datatype="html">
|
||||||
|
<source>Last Run</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">241</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6732151329960766506" datatype="html">
|
<trans-unit id="6732151329960766506" datatype="html">
|
||||||
|
@ -303,12 +303,17 @@ describe('SettingsComponent', () => {
|
|||||||
redis_error:
|
redis_error:
|
||||||
'Error 61 connecting to localhost:6379. Connection refused.',
|
'Error 61 connecting to localhost:6379. Connection refused.',
|
||||||
celery_status: SystemStatusItemStatus.ERROR,
|
celery_status: SystemStatusItemStatus.ERROR,
|
||||||
|
celery_url: 'celery@localhost',
|
||||||
|
celery_error: 'Error connecting to celery@localhost',
|
||||||
index_status: SystemStatusItemStatus.OK,
|
index_status: SystemStatusItemStatus.OK,
|
||||||
index_last_modified: new Date().toISOString(),
|
index_last_modified: new Date().toISOString(),
|
||||||
index_error: null,
|
index_error: null,
|
||||||
classifier_status: SystemStatusItemStatus.OK,
|
classifier_status: SystemStatusItemStatus.OK,
|
||||||
classifier_last_trained: new Date().toISOString(),
|
classifier_last_trained: new Date().toISOString(),
|
||||||
classifier_error: null,
|
classifier_error: null,
|
||||||
|
sanity_check_status: SystemStatusItemStatus.ERROR,
|
||||||
|
sanity_check_last_run: new Date().toISOString(),
|
||||||
|
sanity_check_error: 'Error running sanity check.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||||
@ -320,6 +325,8 @@ describe('SettingsComponent', () => {
|
|||||||
component['systemStatus'].database.status = SystemStatusItemStatus.OK
|
component['systemStatus'].database.status = SystemStatusItemStatus.OK
|
||||||
component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
|
component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
|
||||||
component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
|
component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
|
||||||
|
component['systemStatus'].tasks.sanity_check_status =
|
||||||
|
SystemStatusItemStatus.OK
|
||||||
expect(component.systemStatusHasErrors).toBeFalsy()
|
expect(component.systemStatusHasErrors).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -164,7 +164,10 @@ export class SettingsComponent
|
|||||||
this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
|
this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
|
||||||
this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
|
this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
|
||||||
this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
|
this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
|
||||||
this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
|
this.systemStatus.tasks.classifier_status ===
|
||||||
|
SystemStatusItemStatus.ERROR ||
|
||||||
|
this.systemStatus.tasks.sanity_check_status ===
|
||||||
|
SystemStatusItemStatus.ERROR
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import {
|
import {
|
||||||
PaperlessTask,
|
PaperlessTask,
|
||||||
|
PaperlessTaskName,
|
||||||
PaperlessTaskStatus,
|
PaperlessTaskStatus,
|
||||||
PaperlessTaskType,
|
PaperlessTaskType,
|
||||||
} from 'src/app/data/paperless-task'
|
} from 'src/app/data/paperless-task'
|
||||||
@ -39,7 +40,8 @@ const tasks: PaperlessTask[] = [
|
|||||||
task_file_name: 'test.pdf',
|
task_file_name: 'test.pdf',
|
||||||
date_created: new Date('2023-03-01T10:26:03.093116Z'),
|
date_created: new Date('2023-03-01T10:26:03.093116Z'),
|
||||||
date_done: new Date('2023-03-01T10:26:07.223048Z'),
|
date_done: new Date('2023-03-01T10:26:07.223048Z'),
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Failed,
|
status: PaperlessTaskStatus.Failed,
|
||||||
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
|
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
@ -51,7 +53,8 @@ const tasks: PaperlessTask[] = [
|
|||||||
task_file_name: '191092.pdf',
|
task_file_name: '191092.pdf',
|
||||||
date_created: new Date('2023-03-01T09:26:03.093116Z'),
|
date_created: new Date('2023-03-01T09:26:03.093116Z'),
|
||||||
date_done: new Date('2023-03-01T09:26:07.223048Z'),
|
date_done: new Date('2023-03-01T09:26:07.223048Z'),
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Failed,
|
status: PaperlessTaskStatus.Failed,
|
||||||
result:
|
result:
|
||||||
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
|
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
|
||||||
@ -64,7 +67,8 @@ const tasks: PaperlessTask[] = [
|
|||||||
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
|
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
|
||||||
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
|
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
|
||||||
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
|
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Pending,
|
status: PaperlessTaskStatus.Pending,
|
||||||
result: null,
|
result: null,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
@ -76,7 +80,8 @@ const tasks: PaperlessTask[] = [
|
|||||||
task_file_name: 'paperless-mail-l4dkg8ir',
|
task_file_name: 'paperless-mail-l4dkg8ir',
|
||||||
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
|
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
|
||||||
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
|
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Complete,
|
status: PaperlessTaskStatus.Complete,
|
||||||
result: 'Success. New document id 422 created',
|
result: 'Success. New document id 422 created',
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
@ -88,7 +93,8 @@ const tasks: PaperlessTask[] = [
|
|||||||
task_file_name: 'onlinePaymentSummary.pdf',
|
task_file_name: 'onlinePaymentSummary.pdf',
|
||||||
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
|
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
|
||||||
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
|
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Complete,
|
status: PaperlessTaskStatus.Complete,
|
||||||
result: 'Success. New document id 421 created',
|
result: 'Success. New document id 421 created',
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
@ -100,7 +106,8 @@ const tasks: PaperlessTask[] = [
|
|||||||
task_file_name: 'paperless-mail-_rrpmqk6',
|
task_file_name: 'paperless-mail-_rrpmqk6',
|
||||||
date_created: new Date('2023-06-07T02:54:35.694916Z'),
|
date_created: new Date('2023-06-07T02:54:35.694916Z'),
|
||||||
date_done: null,
|
date_done: null,
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Started,
|
status: PaperlessTaskStatus.Started,
|
||||||
result: null,
|
result: null,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
@ -155,7 +162,9 @@ describe('TasksComponent', () => {
|
|||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
httpTestingController
|
httpTestingController
|
||||||
.expectOne(`${environment.apiBaseUrl}tasks/`)
|
.expectOne(
|
||||||
|
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||||
|
)
|
||||||
.flush(tasks)
|
.flush(tasks)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="modal-basic-title" i18n>System Status</h5>
|
<h6 class="modal-title" id="modal-basic-title" i18n>System Status</h6>
|
||||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
@ -11,11 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
<div class="row row-cols-1 row-cols-md-4 g-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card bg-light h-100">
|
<div class="card bg-light h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0" i18n>Environment</h5>
|
<h6 class="card-title mb-0" i18n>Environment</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="card-text">
|
<dl class="card-text">
|
||||||
@ -38,37 +38,96 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card bg-light h-100">
|
<div class="card bg-light h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0" i18n>Database</h5>
|
<h6 class="card-title mb-0" i18n>Database</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="card-text">
|
<dl class="card-text">
|
||||||
<dt i18n>Type</dt>
|
<dt i18n>Type</dt>
|
||||||
<dd>{{status.database.type}}</dd>
|
<dd>{{status.database.type}}</dd>
|
||||||
<dt i18n>Status</dt>
|
<dt i18n>Status</dt>
|
||||||
<dd class="d-flex align-items-center">
|
<dd>
|
||||||
{{status.database.status}}
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="databaseStatus" triggers="click mouseenter:mouseleave">
|
||||||
@if (status.database.status === 'OK') {
|
{{status.database.status}}
|
||||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.database.url}}" triggers="mouseenter:mouseleave"></i-bs>
|
@if (status.database.status === 'OK') {
|
||||||
} @else {
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.database.url}}: {{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
|
} @else {
|
||||||
}
|
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<ng-template #databaseStatus>
|
||||||
|
@if (status.database.status === 'OK') {
|
||||||
|
{{status.database.url}}
|
||||||
|
} @else {
|
||||||
|
{{status.database.url}}: {{status.database.error}}
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
</dd>
|
</dd>
|
||||||
<dt i18n>Migration Status</dt>
|
<dt i18n>Migration Status</dt>
|
||||||
<dd class="d-flex align-items-center">
|
<dd>
|
||||||
@if (status.database.migration_status.unapplied_migrations.length === 0) {
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="migrationStatus" triggers="click mouseenter:mouseleave">
|
||||||
<ng-container i18n>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
|
@if (status.database.migration_status.unapplied_migrations.length === 0) {
|
||||||
} @else {
|
<ng-container i18n>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
<ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
|
} @else {
|
||||||
}
|
<ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||||
<ng-template #migrationStatus>
|
}
|
||||||
<h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
|
<ng-template #migrationStatus>
|
||||||
@if (status.database.migration_status.unapplied_migrations.length > 0) {
|
<h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
|
||||||
<h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
|
@if (status.database.migration_status.unapplied_migrations.length > 0) {
|
||||||
<ul>
|
<h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
|
||||||
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
|
<ul>
|
||||||
<li class="font-monospace small">{{migration}}</li>
|
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
|
||||||
}
|
<li class="font-monospace small">{{migration}}</li>
|
||||||
</ul>
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</button>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="card bg-light h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="card-title mb-0" i18n>Tasks Queue</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="card-text">
|
||||||
|
<dt i18n>Redis Status</dt>
|
||||||
|
<dd>
|
||||||
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="redisStatus" triggers="click mouseenter:mouseleave">
|
||||||
|
{{status.tasks.redis_status}}
|
||||||
|
@if (status.tasks.redis_status === 'OK') {
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
|
} @else {
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<ng-template #redisStatus>
|
||||||
|
@if (status.tasks.redis_status === 'OK') {
|
||||||
|
{{status.tasks.redis_url}}
|
||||||
|
} @else {
|
||||||
|
{{status.tasks.redis_url}}: {{status.tasks.redis_error}}
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</dd>
|
||||||
|
<dt i18n>Celery Status</dt>
|
||||||
|
<dd>
|
||||||
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="celeryStatus" triggers="click mouseenter:mouseleave">
|
||||||
|
{{status.tasks.celery_status}}
|
||||||
|
@if (status.tasks.celery_status === 'OK') {
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
|
} @else {
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<ng-template #celeryStatus>
|
||||||
|
@if (status.tasks.celery_status === 'OK') {
|
||||||
|
{{status.tasks.celery_url}}
|
||||||
|
} @else {
|
||||||
|
{{status.tasks.celery_error}}
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</dd>
|
</dd>
|
||||||
@ -80,63 +139,109 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card bg-light h-100">
|
<div class="card bg-light h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="card-title mb-0" i18n>Tasks</h5>
|
<h6 class="card-title mb-0" i18n>Health</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<dl class="card-text">
|
<dl class="card-text">
|
||||||
<dt i18n>Redis Status</dt>
|
|
||||||
<dd class="d-flex align-items-center">
|
|
||||||
{{status.tasks.redis_status}}
|
|
||||||
@if (status.tasks.redis_status === 'OK') {
|
|
||||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}" triggers="mouseenter:mouseleave"></i-bs>
|
|
||||||
} @else {
|
|
||||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}: {{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
<dt i18n>Celery Status</dt>
|
|
||||||
<dd class="d-flex align-items-center">
|
|
||||||
{{status.tasks.celery_status}}
|
|
||||||
@if (status.tasks.celery_status === 'OK') {
|
|
||||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
|
||||||
} @else {
|
|
||||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
<dt i18n>Search Index</dt>
|
<dt i18n>Search Index</dt>
|
||||||
<dd class="d-flex align-items-center">
|
<dd class="d-flex align-items-center">
|
||||||
{{status.tasks.index_status}}
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="indexStatus" triggers="click mouseenter:mouseleave">
|
||||||
@if (status.tasks.index_status === 'OK') {
|
{{status.tasks.index_status}}
|
||||||
@if (isStale(status.tasks.index_last_modified)) {
|
@if (status.tasks.index_status === 'OK') {
|
||||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
|
@if (isStale(status.tasks.index_last_modified)) {
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||||
|
} @else {
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
|
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
@if (currentUserIsSuperUser) {
|
||||||
|
@if (isRunning(PaperlessTaskName.IndexOptimize)) {
|
||||||
|
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
|
||||||
|
} @else {
|
||||||
|
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.IndexOptimize)">
|
||||||
|
<i-bs name="play-fill"></i-bs>
|
||||||
|
<ng-container i18n>Run Task</ng-container>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
} @else {
|
|
||||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.index_error}}" triggers="mouseenter:mouseleave"></i-bs>
|
|
||||||
}
|
}
|
||||||
</dd>
|
</dd>
|
||||||
<ng-template #indexStatus>
|
<ng-template #indexStatus>
|
||||||
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
|
@if (status.tasks.index_status === 'OK') {
|
||||||
|
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
|
||||||
|
} @else {
|
||||||
|
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_error}}</span>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<dt i18n>Classifier</dt>
|
<dt i18n>Classifier</dt>
|
||||||
<dd class="d-flex align-items-center">
|
<dd class="d-flex align-items-center">
|
||||||
{{status.tasks.classifier_status}}
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="classifierStatus" triggers="click mouseenter:mouseleave">
|
||||||
@if (status.tasks.classifier_status === 'OK') {
|
{{status.tasks.classifier_status}}
|
||||||
@if (isStale(status.tasks.classifier_last_trained)) {
|
@if (status.tasks.classifier_status === 'OK') {
|
||||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
|
@if (isStale(status.tasks.classifier_last_trained)) {
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||||
|
} @else {
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
<i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
|
<i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
|
||||||
[class.text-danger]="status.tasks.classifier_status === SystemStatusItemStatus.ERROR"
|
[class.text-danger]="status.tasks.classifier_status === SystemStatusItemStatus.ERROR"
|
||||||
[class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"
|
[class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"></i-bs>
|
||||||
ngbPopover="{{status.tasks.classifier_error}}"
|
}
|
||||||
triggers="mouseenter:mouseleave"></i-bs>
|
</button>
|
||||||
|
@if (currentUserIsSuperUser) {
|
||||||
|
@if (isRunning(PaperlessTaskName.TrainClassifier)) {
|
||||||
|
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
|
||||||
|
} @else {
|
||||||
|
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.TrainClassifier)">
|
||||||
|
<i-bs name="play-fill"></i-bs>
|
||||||
|
<ng-container i18n>Run Task</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</dd>
|
</dd>
|
||||||
<ng-template #classifierStatus>
|
<ng-template #classifierStatus>
|
||||||
<h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
|
@if (status.tasks.classifier_status === 'OK') {
|
||||||
|
<h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
|
||||||
|
} @else {
|
||||||
|
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_error}}</span>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
<dt i18n>Sanity Checker</dt>
|
||||||
|
<dd class="d-flex align-items-center">
|
||||||
|
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="sanityCheckerStatus" triggers="click mouseenter:mouseleave">
|
||||||
|
{{status.tasks.sanity_check_status}}
|
||||||
|
@if (status.tasks.sanity_check_status === 'OK') {
|
||||||
|
@if (isStale(status.tasks.sanity_check_last_run)) {
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||||
|
} @else {
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
|
||||||
|
[class.text-danger]="status.tasks.sanity_check_status === SystemStatusItemStatus.ERROR"
|
||||||
|
[class.text-warning]="status.tasks.sanity_check_status === SystemStatusItemStatus.WARNING"></i-bs>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
@if (currentUserIsSuperUser) {
|
||||||
|
@if (isRunning(PaperlessTaskName.SanityCheck)) {
|
||||||
|
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
|
||||||
|
} @else {
|
||||||
|
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.SanityCheck)">
|
||||||
|
<i-bs name="play-fill"></i-bs>
|
||||||
|
<ng-container i18n>Run Task</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
<ng-template #sanityCheckerStatus>
|
||||||
|
@if (status.tasks.sanity_check_status === 'OK') {
|
||||||
|
<h6><ng-container i18n>Last Run</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_last_run | customDate:'medium'}}</span>
|
||||||
|
} @else {
|
||||||
|
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +251,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="copy()">
|
<button class="btn btn-sm d-flex align-items-center btn-dark btn btn-sm d-flex align-items-center btn-dark btn-outline-secondary" (click)="copy()">
|
||||||
@if (!copied) {
|
@if (!copied) {
|
||||||
<i-bs name="clipboard-fill"></i-bs>
|
<i-bs name="clipboard-fill"></i-bs>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.btn.small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
@ -9,11 +9,16 @@ import {
|
|||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { of, throwError } from 'rxjs'
|
||||||
|
import { PaperlessTaskName } from 'src/app/data/paperless-task'
|
||||||
import {
|
import {
|
||||||
InstallType,
|
InstallType,
|
||||||
SystemStatus,
|
SystemStatus,
|
||||||
SystemStatusItemStatus,
|
SystemStatusItemStatus,
|
||||||
} from 'src/app/data/system-status'
|
} from 'src/app/data/system-status'
|
||||||
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
|
import { TasksService } from 'src/app/services/tasks.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { SystemStatusDialogComponent } from './system-status-dialog.component'
|
import { SystemStatusDialogComponent } from './system-status-dialog.component'
|
||||||
|
|
||||||
const status: SystemStatus = {
|
const status: SystemStatus = {
|
||||||
@ -36,12 +41,17 @@ const status: SystemStatus = {
|
|||||||
redis_status: SystemStatusItemStatus.ERROR,
|
redis_status: SystemStatusItemStatus.ERROR,
|
||||||
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
|
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
|
||||||
celery_status: SystemStatusItemStatus.ERROR,
|
celery_status: SystemStatusItemStatus.ERROR,
|
||||||
|
celery_url: 'celery@localhost',
|
||||||
|
celery_error: 'Error connecting to celery@localhost',
|
||||||
index_status: SystemStatusItemStatus.OK,
|
index_status: SystemStatusItemStatus.OK,
|
||||||
index_last_modified: new Date().toISOString(),
|
index_last_modified: new Date().toISOString(),
|
||||||
index_error: null,
|
index_error: null,
|
||||||
classifier_status: SystemStatusItemStatus.OK,
|
classifier_status: SystemStatusItemStatus.OK,
|
||||||
classifier_last_trained: new Date().toISOString(),
|
classifier_last_trained: new Date().toISOString(),
|
||||||
classifier_error: null,
|
classifier_error: null,
|
||||||
|
sanity_check_status: SystemStatusItemStatus.OK,
|
||||||
|
sanity_check_last_run: new Date().toISOString(),
|
||||||
|
sanity_check_error: null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +59,9 @@ describe('SystemStatusDialogComponent', () => {
|
|||||||
let component: SystemStatusDialogComponent
|
let component: SystemStatusDialogComponent
|
||||||
let fixture: ComponentFixture<SystemStatusDialogComponent>
|
let fixture: ComponentFixture<SystemStatusDialogComponent>
|
||||||
let clipboard: Clipboard
|
let clipboard: Clipboard
|
||||||
|
let tasksService: TasksService
|
||||||
|
let systemStatusService: SystemStatusService
|
||||||
|
let toastService: ToastService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@ -67,6 +80,9 @@ describe('SystemStatusDialogComponent', () => {
|
|||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
component.status = status
|
component.status = status
|
||||||
clipboard = TestBed.inject(Clipboard)
|
clipboard = TestBed.inject(Clipboard)
|
||||||
|
tasksService = TestBed.inject(TasksService)
|
||||||
|
systemStatusService = TestBed.inject(SystemStatusService)
|
||||||
|
toastService = TestBed.inject(ToastService)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -93,4 +109,37 @@ describe('SystemStatusDialogComponent', () => {
|
|||||||
expect(component.isStale(date.toISOString())).toBeTruthy()
|
expect(component.isStale(date.toISOString())).toBeTruthy()
|
||||||
expect(component.isStale(date.toISOString(), 26)).toBeFalsy()
|
expect(component.isStale(date.toISOString(), 26)).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should check if task is running', () => {
|
||||||
|
component.runTask(PaperlessTaskName.IndexOptimize)
|
||||||
|
expect(component.isRunning(PaperlessTaskName.IndexOptimize)).toBeTruthy()
|
||||||
|
expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support running tasks, refresh status and show toasts', () => {
|
||||||
|
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
const getStatusSpy = jest.spyOn(systemStatusService, 'get')
|
||||||
|
const runSpy = jest.spyOn(tasksService, 'run')
|
||||||
|
|
||||||
|
// fail first
|
||||||
|
runSpy.mockReturnValue(throwError(() => new Error('error')))
|
||||||
|
component.runTask(PaperlessTaskName.IndexOptimize)
|
||||||
|
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
|
||||||
|
expect(toastErrorSpy).toHaveBeenCalledWith(
|
||||||
|
`Failed to start task ${PaperlessTaskName.IndexOptimize}, see the logs for more details`,
|
||||||
|
expect.any(Error)
|
||||||
|
)
|
||||||
|
|
||||||
|
// succeed
|
||||||
|
runSpy.mockReturnValue(of({}))
|
||||||
|
getStatusSpy.mockReturnValue(of(status))
|
||||||
|
component.runTask(PaperlessTaskName.IndexOptimize)
|
||||||
|
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
|
||||||
|
|
||||||
|
expect(getStatusSpy).toHaveBeenCalled()
|
||||||
|
expect(toastSpy).toHaveBeenCalledWith(
|
||||||
|
`Task ${PaperlessTaskName.IndexOptimize} started`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -7,12 +7,17 @@ import {
|
|||||||
NgbProgressbarModule,
|
NgbProgressbarModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { PaperlessTaskName } from 'src/app/data/paperless-task'
|
||||||
import {
|
import {
|
||||||
SystemStatus,
|
SystemStatus,
|
||||||
SystemStatusItemStatus,
|
SystemStatusItemStatus,
|
||||||
} from 'src/app/data/system-status'
|
} from 'src/app/data/system-status'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
||||||
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
|
import { TasksService } from 'src/app/services/tasks.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-system-status-dialog',
|
selector: 'pngx-system-status-dialog',
|
||||||
@ -30,13 +35,24 @@ import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
|||||||
})
|
})
|
||||||
export class SystemStatusDialogComponent {
|
export class SystemStatusDialogComponent {
|
||||||
public SystemStatusItemStatus = SystemStatusItemStatus
|
public SystemStatusItemStatus = SystemStatusItemStatus
|
||||||
|
public PaperlessTaskName = PaperlessTaskName
|
||||||
public status: SystemStatus
|
public status: SystemStatus
|
||||||
|
|
||||||
public copied: boolean = false
|
public copied: boolean = false
|
||||||
|
|
||||||
|
private runningTasks: Set<PaperlessTaskName> = new Set()
|
||||||
|
|
||||||
|
get currentUserIsSuperUser(): boolean {
|
||||||
|
return this.permissionsService.isSuperUser()
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public activeModal: NgbActiveModal,
|
public activeModal: NgbActiveModal,
|
||||||
private clipboard: Clipboard
|
private clipboard: Clipboard,
|
||||||
|
private systemStatusService: SystemStatusService,
|
||||||
|
private tasksService: TasksService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private permissionsService: PermissionsService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public close() {
|
public close() {
|
||||||
@ -56,4 +72,30 @@ export class SystemStatusDialogComponent {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
|
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isRunning(taskName: PaperlessTaskName): boolean {
|
||||||
|
return this.runningTasks.has(taskName)
|
||||||
|
}
|
||||||
|
|
||||||
|
public runTask(taskName: PaperlessTaskName) {
|
||||||
|
this.runningTasks.add(taskName)
|
||||||
|
this.toastService.showInfo(`Task ${taskName} started`)
|
||||||
|
this.tasksService.run(taskName).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.runningTasks.delete(taskName)
|
||||||
|
this.systemStatusService.get().subscribe({
|
||||||
|
next: (status) => {
|
||||||
|
this.status = status
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.runningTasks.delete(taskName)
|
||||||
|
this.toastService.showError(
|
||||||
|
`Failed to start task ${taskName}, see the logs for more details`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { ObjectWithId } from './object-with-id'
|
import { ObjectWithId } from './object-with-id'
|
||||||
|
|
||||||
export enum PaperlessTaskType {
|
export enum PaperlessTaskType {
|
||||||
// just file tasks, for now
|
Auto = 'auto_task',
|
||||||
File = 'file',
|
ScheduledTask = 'scheduled_task',
|
||||||
|
ManualTask = 'manual_task',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PaperlessTaskName {
|
||||||
|
ConsumeFile = 'consume_file',
|
||||||
|
TrainClassifier = 'train_classifier',
|
||||||
|
SanityCheck = 'check_sanity',
|
||||||
|
IndexOptimize = 'index_optimize',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PaperlessTaskStatus {
|
export enum PaperlessTaskStatus {
|
||||||
@ -23,6 +31,8 @@ export interface PaperlessTask extends ObjectWithId {
|
|||||||
|
|
||||||
task_file_name: string
|
task_file_name: string
|
||||||
|
|
||||||
|
task_name: PaperlessTaskName
|
||||||
|
|
||||||
date_created: Date
|
date_created: Date
|
||||||
|
|
||||||
date_done?: Date
|
date_done?: Date
|
||||||
|
@ -32,11 +32,16 @@ export interface SystemStatus {
|
|||||||
redis_status: SystemStatusItemStatus
|
redis_status: SystemStatusItemStatus
|
||||||
redis_error: string
|
redis_error: string
|
||||||
celery_status: SystemStatusItemStatus
|
celery_status: SystemStatusItemStatus
|
||||||
|
celery_url: string
|
||||||
|
celery_error: string
|
||||||
index_status: SystemStatusItemStatus
|
index_status: SystemStatusItemStatus
|
||||||
index_last_modified: string // ISO date string
|
index_last_modified: string // ISO date string
|
||||||
index_error: string
|
index_error: string
|
||||||
classifier_status: SystemStatusItemStatus
|
classifier_status: SystemStatusItemStatus
|
||||||
classifier_last_trained: string // ISO date string
|
classifier_last_trained: string // ISO date string
|
||||||
classifier_error: string
|
classifier_error: string
|
||||||
|
sanity_check_status: SystemStatusItemStatus
|
||||||
|
sanity_check_last_run: string // ISO date string
|
||||||
|
sanity_check_error: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,11 @@ import {
|
|||||||
} from '@angular/common/http/testing'
|
} from '@angular/common/http/testing'
|
||||||
import { TestBed } from '@angular/core/testing'
|
import { TestBed } from '@angular/core/testing'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { PaperlessTaskStatus, PaperlessTaskType } from '../data/paperless-task'
|
import {
|
||||||
|
PaperlessTaskName,
|
||||||
|
PaperlessTaskStatus,
|
||||||
|
PaperlessTaskType,
|
||||||
|
} from '../data/paperless-task'
|
||||||
import { TasksService } from './tasks.service'
|
import { TasksService } from './tasks.service'
|
||||||
|
|
||||||
describe('TasksService', () => {
|
describe('TasksService', () => {
|
||||||
@ -33,7 +37,7 @@ describe('TasksService', () => {
|
|||||||
it('calls tasks api endpoint on reload', () => {
|
it('calls tasks api endpoint on reload', () => {
|
||||||
tasksService.reload()
|
tasksService.reload()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}tasks/`
|
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
@ -41,7 +45,9 @@ describe('TasksService', () => {
|
|||||||
it('does not call tasks api endpoint on reload if already loading', () => {
|
it('does not call tasks api endpoint on reload if already loading', () => {
|
||||||
tasksService.loading = true
|
tasksService.loading = true
|
||||||
tasksService.reload()
|
tasksService.reload()
|
||||||
httpTestingController.expectNone(`${environment.apiBaseUrl}tasks/`)
|
httpTestingController.expectNone(
|
||||||
|
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
||||||
@ -55,14 +61,19 @@ describe('TasksService', () => {
|
|||||||
})
|
})
|
||||||
req.flush([])
|
req.flush([])
|
||||||
// reload is then called
|
// reload is then called
|
||||||
httpTestingController.expectOne(`${environment.apiBaseUrl}tasks/`).flush([])
|
httpTestingController
|
||||||
|
.expectOne(
|
||||||
|
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||||
|
)
|
||||||
|
.flush([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sorts tasks returned from api', () => {
|
it('sorts tasks returned from api', () => {
|
||||||
expect(tasksService.total).toEqual(0)
|
expect(tasksService.total).toEqual(0)
|
||||||
const mockTasks = [
|
const mockTasks = [
|
||||||
{
|
{
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Complete,
|
status: PaperlessTaskStatus.Complete,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
task_id: '1234',
|
task_id: '1234',
|
||||||
@ -70,7 +81,8 @@ describe('TasksService', () => {
|
|||||||
date_created: new Date(),
|
date_created: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Failed,
|
status: PaperlessTaskStatus.Failed,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
task_id: '1235',
|
task_id: '1235',
|
||||||
@ -78,7 +90,8 @@ describe('TasksService', () => {
|
|||||||
date_created: new Date(),
|
date_created: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Pending,
|
status: PaperlessTaskStatus.Pending,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
task_id: '1236',
|
task_id: '1236',
|
||||||
@ -86,7 +99,8 @@ describe('TasksService', () => {
|
|||||||
date_created: new Date(),
|
date_created: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Started,
|
status: PaperlessTaskStatus.Started,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
task_id: '1237',
|
task_id: '1237',
|
||||||
@ -94,7 +108,8 @@ describe('TasksService', () => {
|
|||||||
date_created: new Date(),
|
date_created: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: PaperlessTaskType.File,
|
type: PaperlessTaskType.Auto,
|
||||||
|
task_name: PaperlessTaskName.ConsumeFile,
|
||||||
status: PaperlessTaskStatus.Complete,
|
status: PaperlessTaskStatus.Complete,
|
||||||
acknowledged: false,
|
acknowledged: false,
|
||||||
task_id: '1238',
|
task_id: '1238',
|
||||||
@ -106,7 +121,7 @@ describe('TasksService', () => {
|
|||||||
tasksService.reload()
|
tasksService.reload()
|
||||||
|
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}tasks/`
|
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||||
)
|
)
|
||||||
|
|
||||||
req.flush(mockTasks)
|
req.flush(mockTasks)
|
||||||
@ -117,4 +132,19 @@ describe('TasksService', () => {
|
|||||||
expect(tasksService.queuedFileTasks).toHaveLength(1)
|
expect(tasksService.queuedFileTasks).toHaveLength(1)
|
||||||
expect(tasksService.startedFileTasks).toHaveLength(1)
|
expect(tasksService.startedFileTasks).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('supports running tasks', () => {
|
||||||
|
tasksService.run(PaperlessTaskName.SanityCheck).subscribe((res) => {
|
||||||
|
expect(res).toEqual({
|
||||||
|
result: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}tasks/run/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('POST')
|
||||||
|
req.flush({
|
||||||
|
result: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
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 } from 'rxjs/operators'
|
||||||
import {
|
import {
|
||||||
PaperlessTask,
|
PaperlessTask,
|
||||||
|
PaperlessTaskName,
|
||||||
PaperlessTaskStatus,
|
PaperlessTaskStatus,
|
||||||
PaperlessTaskType,
|
|
||||||
} from 'src/app/data/paperless-task'
|
} from 'src/app/data/paperless-task'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { environment } from 'src/environments/environment'
|
|||||||
})
|
})
|
||||||
export class TasksService {
|
export class TasksService {
|
||||||
private baseUrl: string = environment.apiBaseUrl
|
private baseUrl: string = environment.apiBaseUrl
|
||||||
|
private endpoint: string = 'tasks'
|
||||||
|
|
||||||
public loading: boolean
|
public loading: boolean
|
||||||
|
|
||||||
@ -54,10 +55,14 @@ export class TasksService {
|
|||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
this.http
|
this.http
|
||||||
.get<PaperlessTask[]>(`${this.baseUrl}tasks/`)
|
.get<PaperlessTask[]>(
|
||||||
|
`${this.baseUrl}${this.endpoint}/?task_name=consume_file&acknowledged=false`
|
||||||
|
)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifer), first())
|
.pipe(takeUntil(this.unsubscribeNotifer), first())
|
||||||
.subscribe((r) => {
|
.subscribe((r) => {
|
||||||
this.fileTasks = r.filter((t) => t.type == PaperlessTaskType.File) // they're all File tasks, for now
|
this.fileTasks = r.filter(
|
||||||
|
(t) => t.task_name == PaperlessTaskName.ConsumeFile
|
||||||
|
)
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -76,4 +81,13 @@ export class TasksService {
|
|||||||
public cancelPending(): void {
|
public cancelPending(): void {
|
||||||
this.unsubscribeNotifer.next(true)
|
this.unsubscribeNotifer.next(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public run(taskName: PaperlessTaskName): Observable<any> {
|
||||||
|
return this.http.post<any>(
|
||||||
|
`${environment.apiBaseUrl}${this.endpoint}/run/`,
|
||||||
|
{
|
||||||
|
task_name: taskName,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,7 @@ import {
|
|||||||
personFillLock,
|
personFillLock,
|
||||||
personLock,
|
personLock,
|
||||||
personSquare,
|
personSquare,
|
||||||
|
playFill,
|
||||||
plus,
|
plus,
|
||||||
plusCircle,
|
plusCircle,
|
||||||
questionCircle,
|
questionCircle,
|
||||||
@ -312,6 +313,7 @@ const icons = {
|
|||||||
personFillLock,
|
personFillLock,
|
||||||
personLock,
|
personLock,
|
||||||
personSquare,
|
personSquare,
|
||||||
|
playFill,
|
||||||
plus,
|
plus,
|
||||||
plusCircle,
|
plusCircle,
|
||||||
questionCircle,
|
questionCircle,
|
||||||
|
@ -21,10 +21,12 @@
|
|||||||
--pngx-success-darken-10: hsl(152, 69%, 11%); // based on success #198754
|
--pngx-success-darken-10: hsl(152, 69%, 11%); // based on success #198754
|
||||||
--pngx-bg-alt: #fff;
|
--pngx-bg-alt: #fff;
|
||||||
--pngx-bg-darker: var(--bs-gray-100);
|
--pngx-bg-darker: var(--bs-gray-100);
|
||||||
--pngx-bg-alt2: var(--bs-gray-200);
|
--pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
|
||||||
--pngx-bg-disabled: #f7f7f7;
|
--pngx-bg-disabled: #f7f7f7;
|
||||||
--pngx-focus-alpha: 0.3;
|
--pngx-focus-alpha: 0.3;
|
||||||
--pngx-toast-max-width: 360px;
|
--pngx-toast-max-width: 360px;
|
||||||
|
--bs-info: var(--pngx-bg-alt2);
|
||||||
|
--bs-info-rgb: 233, 236, 239;
|
||||||
@media screen and (min-width: 1024px) {
|
@media screen and (min-width: 1024px) {
|
||||||
--pngx-toast-max-width: 450px;
|
--pngx-toast-max-width: 450px;
|
||||||
}
|
}
|
||||||
@ -71,8 +73,15 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-mode {
|
@mixin dark-mode {
|
||||||
--bs-body-color: #{$text-color-dark-bg};
|
|
||||||
--pngx-body-color-accent: #{$text-color-dark-bg-accent};
|
--pngx-body-color-accent: #{$text-color-dark-bg-accent};
|
||||||
|
--pngx-bg-alt: #242529;
|
||||||
|
--pngx-bg-alt2: #232323;
|
||||||
|
--pngx-bg-darker: #101216;
|
||||||
|
--pngx-bg-disabled: var(--pngx-bg-alt);
|
||||||
|
--pngx-focus-alpha: 0.6;
|
||||||
|
--pngx-primary-faded: var(--pngx-primary-darken-15);
|
||||||
|
--pngx-primary-text-contrast: var(--bs-body-color);
|
||||||
|
--bs-body-color: #{$text-color-dark-bg};
|
||||||
--bs-secondary-color: #6c757d;
|
--bs-secondary-color: #6c757d;
|
||||||
--bs-danger: #b71631;
|
--bs-danger: #b71631;
|
||||||
--bs-danger-rgb: 183, 22, 49;
|
--bs-danger-rgb: 183, 22, 49;
|
||||||
@ -80,15 +89,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
--bs-body-bg-rgb: 22, 22, 24;
|
--bs-body-bg-rgb: 22, 22, 24;
|
||||||
--bs-light: #1c1c1f;
|
--bs-light: #1c1c1f;
|
||||||
--bs-light-rgb: 28, 28, 31;
|
--bs-light-rgb: 28, 28, 31;
|
||||||
|
--bs-info: var(--pngx-bg-alt);
|
||||||
|
--bs-info-rgb: 36, 36, 39;
|
||||||
--bs-border-color: #47494f;
|
--bs-border-color: #47494f;
|
||||||
--pngx-bg-alt2: #232323;
|
|
||||||
--pngx-bg-darker: #101216;
|
|
||||||
--bs-tertiary-bg: var(--pngx-bg-darker);
|
--bs-tertiary-bg: var(--pngx-bg-darker);
|
||||||
--pngx-bg-alt: #242529;
|
|
||||||
--pngx-bg-disabled: var(--pngx-bg-alt);
|
|
||||||
--pngx-focus-alpha: 0.6;
|
|
||||||
--pngx-primary-faded: var(--pngx-primary-darken-15);
|
|
||||||
--pngx-primary-text-contrast: var(--bs-body-color);
|
|
||||||
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
||||||
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import warnings
|
import warnings
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -144,19 +143,6 @@ class DocumentClassifier:
|
|||||||
):
|
):
|
||||||
raise IncompatibleClassifierVersionError("sklearn version update")
|
raise IncompatibleClassifierVersionError("sklearn version update")
|
||||||
|
|
||||||
def set_last_checked(self) -> None:
|
|
||||||
# save a timestamp of the last time we checked for retraining to a file
|
|
||||||
with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("w") as f:
|
|
||||||
f.write(str(time.time()))
|
|
||||||
|
|
||||||
def get_last_checked(self) -> float | None:
|
|
||||||
# load the timestamp of the last time we checked for retraining
|
|
||||||
try:
|
|
||||||
with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("r") as f:
|
|
||||||
return float(f.read())
|
|
||||||
except FileNotFoundError: # pragma: no cover
|
|
||||||
return None
|
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
target_file: Path = settings.MODEL_FILE
|
target_file: Path = settings.MODEL_FILE
|
||||||
target_file_temp: Path = target_file.with_suffix(".pickle.part")
|
target_file_temp: Path = target_file.with_suffix(".pickle.part")
|
||||||
@ -177,7 +163,6 @@ class DocumentClassifier:
|
|||||||
pickle.dump(self.storage_path_classifier, f)
|
pickle.dump(self.storage_path_classifier, f)
|
||||||
|
|
||||||
target_file_temp.rename(target_file)
|
target_file_temp.rename(target_file)
|
||||||
self.set_last_checked()
|
|
||||||
|
|
||||||
def train(self) -> bool:
|
def train(self) -> bool:
|
||||||
# Get non-inbox documents
|
# Get non-inbox documents
|
||||||
@ -246,7 +231,6 @@ class DocumentClassifier:
|
|||||||
and self.last_doc_change_time >= latest_doc_change
|
and self.last_doc_change_time >= latest_doc_change
|
||||||
) and self.last_auto_type_hash == hasher.digest():
|
) and self.last_auto_type_hash == hasher.digest():
|
||||||
logger.info("No updates since last training")
|
logger.info("No updates since last training")
|
||||||
self.set_last_checked()
|
|
||||||
# Set the classifier information into the cache
|
# Set the classifier information into the cache
|
||||||
# Caching for 50 minutes, so slightly less than the normal retrain time
|
# Caching for 50 minutes, so slightly less than the normal retrain time
|
||||||
cache.set(
|
cache.set(
|
||||||
|
@ -37,6 +37,7 @@ from documents.models import CustomFieldInstance
|
|||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import Log
|
from documents.models import Log
|
||||||
|
from documents.models import PaperlessTask
|
||||||
from documents.models import ShareLink
|
from documents.models import ShareLink
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
@ -775,6 +776,21 @@ class ShareLinkFilterSet(FilterSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessTaskFilterSet(FilterSet):
|
||||||
|
acknowledged = BooleanFilter(
|
||||||
|
label="Acknowledged",
|
||||||
|
field_name="acknowledged",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PaperlessTask
|
||||||
|
fields = {
|
||||||
|
"type": ["exact"],
|
||||||
|
"task_name": ["exact"],
|
||||||
|
"status": ["exact"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
|
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
|
||||||
"""
|
"""
|
||||||
A filter backend that limits results to those where the requesting user
|
A filter backend that limits results to those where the requesting user
|
||||||
|
@ -10,4 +10,4 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
train_classifier()
|
train_classifier(scheduled=False)
|
||||||
|
@ -12,6 +12,6 @@ class Command(ProgressBarMixin, BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
self.handle_progress_bar_mixin(**options)
|
self.handle_progress_bar_mixin(**options)
|
||||||
messages = check_sanity(progress=self.use_progress_bar)
|
messages = check_sanity(progress=self.use_progress_bar, scheduled=False)
|
||||||
|
|
||||||
messages.log_messages()
|
messages.log_messages()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.1.6 on 2025-02-20 04:55
|
# Generated by Django 5.1.6 on 2025-02-21 16:34
|
||||||
|
|
||||||
import multiselectfield.db.fields
|
import multiselectfield.db.fields
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
@ -16,12 +16,52 @@ def update_workflow_sources(apps, schema_editor):
|
|||||||
trigger.save()
|
trigger.save()
|
||||||
|
|
||||||
|
|
||||||
|
def make_existing_tasks_consume_auto(apps, schema_editor):
|
||||||
|
PaperlessTask = apps.get_model("documents", "PaperlessTask")
|
||||||
|
PaperlessTask.objects.all().update(type="auto_task", task_name="consume_file")
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("documents", "1062_alter_savedviewfilterrule_rule_type"),
|
("documents", "1062_alter_savedviewfilterrule_rule_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="paperlesstask",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("auto_task", "Auto Task"),
|
||||||
|
("scheduled_task", "Scheduled Task"),
|
||||||
|
("manual_task", "Manual Task"),
|
||||||
|
],
|
||||||
|
default="auto_task",
|
||||||
|
help_text="The type of task that was run",
|
||||||
|
max_length=30,
|
||||||
|
verbose_name="Task Type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="paperlesstask",
|
||||||
|
name="task_name",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("consume_file", "Consume File"),
|
||||||
|
("train_classifier", "Train Classifier"),
|
||||||
|
("check_sanity", "Check Sanity"),
|
||||||
|
("index_optimize", "Index Optimize"),
|
||||||
|
],
|
||||||
|
help_text="Name of the task that was run",
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Task Name",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=make_existing_tasks_consume_auto,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="workflowactionwebhook",
|
model_name="workflowactionwebhook",
|
||||||
name="url",
|
name="url",
|
@ -650,6 +650,17 @@ class PaperlessTask(ModelWithOwner):
|
|||||||
ALL_STATES = sorted(states.ALL_STATES)
|
ALL_STATES = sorted(states.ALL_STATES)
|
||||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||||
|
|
||||||
|
class TaskType(models.TextChoices):
|
||||||
|
AUTO = ("auto_task", _("Auto Task"))
|
||||||
|
SCHEDULED_TASK = ("scheduled_task", _("Scheduled Task"))
|
||||||
|
MANUAL_TASK = ("manual_task", _("Manual Task"))
|
||||||
|
|
||||||
|
class TaskName(models.TextChoices):
|
||||||
|
CONSUME_FILE = ("consume_file", _("Consume File"))
|
||||||
|
TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier"))
|
||||||
|
CHECK_SANITY = ("check_sanity", _("Check Sanity"))
|
||||||
|
INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize"))
|
||||||
|
|
||||||
task_id = models.CharField(
|
task_id = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
@ -673,8 +684,9 @@ class PaperlessTask(ModelWithOwner):
|
|||||||
task_name = models.CharField(
|
task_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
choices=TaskName.choices,
|
||||||
verbose_name=_("Task Name"),
|
verbose_name=_("Task Name"),
|
||||||
help_text=_("Name of the Task which was run"),
|
help_text=_("Name of the task that was run"),
|
||||||
)
|
)
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
@ -684,24 +696,28 @@ class PaperlessTask(ModelWithOwner):
|
|||||||
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,
|
||||||
@ -711,6 +727,14 @@ class PaperlessTask(ModelWithOwner):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=TaskType.choices,
|
||||||
|
default=TaskType.AUTO,
|
||||||
|
verbose_name=_("Task Type"),
|
||||||
|
help_text=_("The type of task that was run"),
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Task {self.task_id}"
|
return f"Task {self.task_id}"
|
||||||
|
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
from celery import states
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from documents.models import PaperlessTask
|
||||||
|
|
||||||
|
|
||||||
class SanityCheckMessages:
|
class SanityCheckMessages:
|
||||||
@ -57,7 +61,17 @@ class SanityCheckFailedException(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def check_sanity(*, progress=False) -> SanityCheckMessages:
|
def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages:
|
||||||
|
paperless_task = PaperlessTask.objects.create(
|
||||||
|
task_id=uuid.uuid4(),
|
||||||
|
type=PaperlessTask.TaskType.SCHEDULED_TASK
|
||||||
|
if scheduled
|
||||||
|
else PaperlessTask.TaskType.MANUAL_TASK,
|
||||||
|
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||||
|
status=states.STARTED,
|
||||||
|
date_created=timezone.now(),
|
||||||
|
date_started=timezone.now(),
|
||||||
|
)
|
||||||
messages = SanityCheckMessages()
|
messages = SanityCheckMessages()
|
||||||
|
|
||||||
present_files = {
|
present_files = {
|
||||||
@ -142,4 +156,11 @@ def check_sanity(*, progress=False) -> SanityCheckMessages:
|
|||||||
for extra_file in present_files:
|
for extra_file in present_files:
|
||||||
messages.warning(None, f"Orphaned file in media dir: {extra_file}")
|
messages.warning(None, f"Orphaned file in media dir: {extra_file}")
|
||||||
|
|
||||||
|
paperless_task.status = states.SUCCESS if not messages.has_error else states.FAILURE
|
||||||
|
# result is concatenated messages
|
||||||
|
paperless_task.result = f"{len(messages)} issues found."
|
||||||
|
if messages.has_error:
|
||||||
|
paperless_task.result += " Check logs for details."
|
||||||
|
paperless_task.date_done = timezone.now()
|
||||||
|
paperless_task.save(update_fields=["status", "result", "date_done"])
|
||||||
return messages
|
return messages
|
||||||
|
@ -1710,6 +1710,7 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
|||||||
fields = (
|
fields = (
|
||||||
"id",
|
"id",
|
||||||
"task_id",
|
"task_id",
|
||||||
|
"task_name",
|
||||||
"task_file_name",
|
"task_file_name",
|
||||||
"date_created",
|
"date_created",
|
||||||
"date_done",
|
"date_done",
|
||||||
@ -1721,12 +1722,6 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
|||||||
"owner",
|
"owner",
|
||||||
)
|
)
|
||||||
|
|
||||||
type = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_type(self, obj) -> str:
|
|
||||||
# just file tasks, for now
|
|
||||||
return "file"
|
|
||||||
|
|
||||||
related_document = serializers.SerializerMethodField()
|
related_document = serializers.SerializerMethodField()
|
||||||
created_doc_re = re.compile(r"New document id (\d+) created")
|
created_doc_re = re.compile(r"New document id (\d+) created")
|
||||||
duplicate_doc_re = re.compile(r"It is a duplicate of .* \(#(\d+)\)")
|
duplicate_doc_re = re.compile(r"It is a duplicate of .* \(#(\d+)\)")
|
||||||
@ -1734,24 +1729,33 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
|||||||
def get_related_document(self, obj) -> str | None:
|
def get_related_document(self, obj) -> str | None:
|
||||||
result = None
|
result = None
|
||||||
re = None
|
re = None
|
||||||
match obj.status:
|
if obj.result:
|
||||||
case states.SUCCESS:
|
match obj.status:
|
||||||
re = self.created_doc_re
|
case states.SUCCESS:
|
||||||
case states.FAILURE:
|
re = self.created_doc_re
|
||||||
re = (
|
case states.FAILURE:
|
||||||
self.duplicate_doc_re
|
re = (
|
||||||
if "existing document is in the trash" not in obj.result
|
self.duplicate_doc_re
|
||||||
else None
|
if "existing document is in the trash" not in obj.result
|
||||||
)
|
else None
|
||||||
if re is not None:
|
)
|
||||||
try:
|
if re is not None:
|
||||||
result = re.search(obj.result).group(1)
|
try:
|
||||||
except Exception:
|
result = re.search(obj.result).group(1)
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class RunTaskViewSerializer(serializers.Serializer):
|
||||||
|
task_name = serializers.ChoiceField(
|
||||||
|
choices=PaperlessTask.TaskName.choices,
|
||||||
|
label="Task Name",
|
||||||
|
write_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AcknowledgeTasksViewSerializer(serializers.Serializer):
|
class AcknowledgeTasksViewSerializer(serializers.Serializer):
|
||||||
tasks = serializers.ListField(
|
tasks = serializers.ListField(
|
||||||
required=True,
|
required=True,
|
||||||
|
@ -1255,10 +1255,11 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs):
|
|||||||
user_id = overrides.owner_id if overrides else None
|
user_id = overrides.owner_id if overrides else None
|
||||||
|
|
||||||
PaperlessTask.objects.create(
|
PaperlessTask.objects.create(
|
||||||
|
type=PaperlessTask.TaskType.AUTO,
|
||||||
task_id=headers["id"],
|
task_id=headers["id"],
|
||||||
status=states.PENDING,
|
status=states.PENDING,
|
||||||
task_file_name=task_file_name,
|
task_file_name=task_file_name,
|
||||||
task_name=headers["task"],
|
task_name=PaperlessTask.TaskName.CONSUME_FILE,
|
||||||
result=None,
|
result=None,
|
||||||
date_created=timezone.now(),
|
date_created=timezone.now(),
|
||||||
date_started=None,
|
date_started=None,
|
||||||
|
@ -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.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -35,6 +36,7 @@ from documents.models import Correspondent
|
|||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
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.models import Workflow
|
from documents.models import Workflow
|
||||||
@ -74,19 +76,34 @@ def index_reindex(*, progress_bar_disable=False):
|
|||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def train_classifier():
|
def train_classifier(*, scheduled=True):
|
||||||
|
task = PaperlessTask.objects.create(
|
||||||
|
type=PaperlessTask.TaskType.SCHEDULED_TASK
|
||||||
|
if scheduled
|
||||||
|
else PaperlessTask.TaskType.MANUAL_TASK,
|
||||||
|
task_id=uuid.uuid4(),
|
||||||
|
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||||
|
status=states.STARTED,
|
||||||
|
date_created=timezone.now(),
|
||||||
|
date_started=timezone.now(),
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
not Tag.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
not Tag.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||||
and not DocumentType.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
and not DocumentType.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||||
and not Correspondent.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
and not Correspondent.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||||
and not StoragePath.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
and not StoragePath.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||||
):
|
):
|
||||||
logger.info("No automatic matching items, not training")
|
result = "No automatic matching items, not training"
|
||||||
|
logger.info(result)
|
||||||
# Special case, items were once auto and trained, so remove the model
|
# Special case, items were once auto and trained, so remove the model
|
||||||
# and prevent its use again
|
# and prevent its use again
|
||||||
if settings.MODEL_FILE.exists():
|
if settings.MODEL_FILE.exists():
|
||||||
logger.info(f"Removing {settings.MODEL_FILE} so it won't be used")
|
logger.info(f"Removing {settings.MODEL_FILE} so it won't be used")
|
||||||
settings.MODEL_FILE.unlink()
|
settings.MODEL_FILE.unlink()
|
||||||
|
task.status = states.SUCCESS
|
||||||
|
task.result = result
|
||||||
|
task.date_done = timezone.now()
|
||||||
|
task.save()
|
||||||
return
|
return
|
||||||
|
|
||||||
classifier = load_classifier()
|
classifier = load_classifier()
|
||||||
@ -100,11 +117,19 @@ def train_classifier():
|
|||||||
f"Saving updated classifier model to {settings.MODEL_FILE}...",
|
f"Saving updated classifier model to {settings.MODEL_FILE}...",
|
||||||
)
|
)
|
||||||
classifier.save()
|
classifier.save()
|
||||||
|
task.result = "Training completed successfully"
|
||||||
else:
|
else:
|
||||||
logger.debug("Training data unchanged.")
|
logger.debug("Training data unchanged.")
|
||||||
|
task.result = "Training data unchanged"
|
||||||
|
|
||||||
|
task.status = states.SUCCESS
|
||||||
|
task.date_done = timezone.now()
|
||||||
|
task.save(update_fields=["status", "result", "date_done"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Classifier error: " + str(e))
|
logger.warning("Classifier error: " + str(e))
|
||||||
|
task.status = states.FAILURE
|
||||||
|
task.result = str(e)
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True)
|
@shared_task(bind=True)
|
||||||
@ -176,13 +201,16 @@ def consume_file(
|
|||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def sanity_check():
|
def sanity_check(*, scheduled=True, raise_on_error=True):
|
||||||
messages = sanity_checker.check_sanity()
|
messages = sanity_checker.check_sanity(scheduled=scheduled)
|
||||||
|
|
||||||
messages.log_messages()
|
messages.log_messages()
|
||||||
|
|
||||||
if messages.has_error:
|
if messages.has_error:
|
||||||
raise SanityCheckFailedException("Sanity check failed with errors. See log.")
|
message = "Sanity check exited with errors. See log."
|
||||||
|
if raise_on_error:
|
||||||
|
raise SanityCheckFailedException(message)
|
||||||
|
return message
|
||||||
elif messages.has_warning:
|
elif messages.has_warning:
|
||||||
return "Sanity check exited with warnings. See log."
|
return "Sanity check exited with warnings. See log."
|
||||||
elif len(messages) > 0:
|
elif len(messages) > 0:
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from celery import states
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.classifier import ClassifierModelCorruptError
|
from documents.models import PaperlessTask
|
||||||
from documents.classifier import DocumentClassifier
|
|
||||||
from documents.classifier import load_classifier
|
|
||||||
from documents.models import Document
|
|
||||||
from documents.models import Tag
|
|
||||||
from paperless import version
|
from paperless import version
|
||||||
|
|
||||||
|
|
||||||
@ -193,7 +189,6 @@ class TestSystemStatus(APITestCase):
|
|||||||
self.assertEqual(response.data["tasks"]["index_status"], "ERROR")
|
self.assertEqual(response.data["tasks"]["index_status"], "ERROR")
|
||||||
self.assertIsNotNone(response.data["tasks"]["index_error"])
|
self.assertIsNotNone(response.data["tasks"]["index_error"])
|
||||||
|
|
||||||
@override_settings(DATA_DIR=Path("/tmp/does_not_exist/data/"))
|
|
||||||
def test_system_status_classifier_ok(self):
|
def test_system_status_classifier_ok(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -203,9 +198,11 @@ class TestSystemStatus(APITestCase):
|
|||||||
THEN:
|
THEN:
|
||||||
- The response contains an OK classifier status
|
- The response contains an OK classifier status
|
||||||
"""
|
"""
|
||||||
load_classifier()
|
PaperlessTask.objects.create(
|
||||||
test_classifier = DocumentClassifier()
|
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||||
test_classifier.save()
|
status=states.SUCCESS,
|
||||||
|
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||||
|
)
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@ -215,73 +212,101 @@ class TestSystemStatus(APITestCase):
|
|||||||
def test_system_status_classifier_warning(self):
|
def test_system_status_classifier_warning(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- The classifier does not exist yet
|
- No classifier task is found
|
||||||
- > 0 documents and tags with auto matching exist
|
|
||||||
WHEN:
|
WHEN:
|
||||||
- The user requests the system status
|
- The user requests the system status
|
||||||
THEN:
|
THEN:
|
||||||
- The response contains an WARNING classifier status
|
- The response contains a WARNING classifier status
|
||||||
"""
|
"""
|
||||||
with override_settings(MODEL_FILE=Path("does_not_exist")):
|
self.client.force_login(self.user)
|
||||||
Document.objects.create(
|
response = self.client.get(self.ENDPOINT)
|
||||||
title="Test Document",
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
)
|
self.assertEqual(
|
||||||
Tag.objects.create(name="Test Tag", matching_algorithm=Tag.MATCH_AUTO)
|
response.data["tasks"]["classifier_status"],
|
||||||
self.client.force_login(self.user)
|
"WARNING",
|
||||||
response = self.client.get(self.ENDPOINT)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(response.data["tasks"]["classifier_status"], "WARNING")
|
|
||||||
self.assertIsNotNone(response.data["tasks"]["classifier_error"])
|
|
||||||
|
|
||||||
@mock.patch(
|
def test_system_status_classifier_error(self):
|
||||||
"documents.classifier.load_classifier",
|
|
||||||
side_effect=ClassifierModelCorruptError(),
|
|
||||||
)
|
|
||||||
def test_system_status_classifier_error(self, mock_load_classifier):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- The classifier does exist but is corrupt
|
- An error occurred while loading the classifier
|
||||||
- > 0 documents and tags with auto matching exist
|
|
||||||
WHEN:
|
WHEN:
|
||||||
- The user requests the system status
|
- The user requests the system status
|
||||||
THEN:
|
THEN:
|
||||||
- The response contains an ERROR classifier status
|
- The response contains an ERROR classifier status
|
||||||
"""
|
"""
|
||||||
with (
|
PaperlessTask.objects.create(
|
||||||
tempfile.NamedTemporaryFile(
|
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||||
dir="/tmp",
|
status=states.FAILURE,
|
||||||
delete=False,
|
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||||
) as does_exist,
|
result="Classifier training failed",
|
||||||
override_settings(MODEL_FILE=Path(does_exist.name)),
|
)
|
||||||
):
|
self.client.force_login(self.user)
|
||||||
Document.objects.create(
|
response = self.client.get(self.ENDPOINT)
|
||||||
title="Test Document",
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
)
|
self.assertEqual(
|
||||||
Tag.objects.create(
|
response.data["tasks"]["classifier_status"],
|
||||||
name="Test Tag",
|
"ERROR",
|
||||||
matching_algorithm=Tag.MATCH_AUTO,
|
)
|
||||||
)
|
self.assertIsNotNone(response.data["tasks"]["classifier_error"])
|
||||||
self.client.force_login(self.user)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertEqual(
|
|
||||||
response.data["tasks"]["classifier_status"],
|
|
||||||
"ERROR",
|
|
||||||
)
|
|
||||||
self.assertIsNotNone(response.data["tasks"]["classifier_error"])
|
|
||||||
|
|
||||||
def test_system_status_classifier_ok_no_objects(self):
|
def test_system_status_sanity_check_ok(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- The classifier does not exist (and should not)
|
- The sanity check is successful
|
||||||
- No documents nor objects with auto matching exist
|
|
||||||
WHEN:
|
WHEN:
|
||||||
- The user requests the system status
|
- The user requests the system status
|
||||||
THEN:
|
THEN:
|
||||||
- The response contains an OK classifier status
|
- The response contains an OK sanity check status
|
||||||
"""
|
"""
|
||||||
with override_settings(MODEL_FILE=Path("does_not_exist")):
|
PaperlessTask.objects.create(
|
||||||
self.client.force_login(self.user)
|
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||||
response = self.client.get(self.ENDPOINT)
|
status=states.SUCCESS,
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||||
self.assertEqual(response.data["tasks"]["classifier_status"], "OK")
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["tasks"]["sanity_check_status"], "OK")
|
||||||
|
self.assertIsNone(response.data["tasks"]["sanity_check_error"])
|
||||||
|
|
||||||
|
def test_system_status_sanity_check_warning(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- No sanity check task is found
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains a WARNING sanity check status
|
||||||
|
"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["tasks"]["sanity_check_status"],
|
||||||
|
"WARNING",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_system_status_sanity_check_error(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The sanity check failed
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains an ERROR sanity check status
|
||||||
|
"""
|
||||||
|
PaperlessTask.objects.create(
|
||||||
|
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||||
|
status=states.FAILURE,
|
||||||
|
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||||
|
result="5 issues found.",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["tasks"]["sanity_check_status"],
|
||||||
|
"ERROR",
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(response.data["tasks"]["sanity_check_error"])
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import celery
|
import celery
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
@ -8,6 +9,7 @@ from rest_framework.test import APITestCase
|
|||||||
|
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from documents.views import TasksViewSet
|
||||||
|
|
||||||
|
|
||||||
class TestTasks(DirectoriesMixin, APITestCase):
|
class TestTasks(DirectoriesMixin, APITestCase):
|
||||||
@ -130,7 +132,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT + "?acknowledged=false")
|
||||||
self.assertEqual(len(response.data), 0)
|
self.assertEqual(len(response.data), 0)
|
||||||
|
|
||||||
def test_tasks_owner_aware(self):
|
def test_tasks_owner_aware(self):
|
||||||
@ -246,7 +248,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
|||||||
PaperlessTask.objects.create(
|
PaperlessTask.objects.create(
|
||||||
task_id=str(uuid.uuid4()),
|
task_id=str(uuid.uuid4()),
|
||||||
task_file_name="test.pdf",
|
task_file_name="test.pdf",
|
||||||
task_name="documents.tasks.some_task",
|
task_name=PaperlessTask.TaskName.CONSUME_FILE,
|
||||||
status=celery.states.SUCCESS,
|
status=celery.states.SUCCESS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -272,7 +274,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
|||||||
PaperlessTask.objects.create(
|
PaperlessTask.objects.create(
|
||||||
task_id=str(uuid.uuid4()),
|
task_id=str(uuid.uuid4()),
|
||||||
task_file_name="anothertest.pdf",
|
task_file_name="anothertest.pdf",
|
||||||
task_name="documents.tasks.some_task",
|
task_name=PaperlessTask.TaskName.CONSUME_FILE,
|
||||||
status=celery.states.SUCCESS,
|
status=celery.states.SUCCESS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -309,3 +311,62 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
|||||||
returned_data = response.data[0]
|
returned_data = response.data[0]
|
||||||
|
|
||||||
self.assertEqual(returned_data["related_document"], "1234")
|
self.assertEqual(returned_data["related_document"], "1234")
|
||||||
|
|
||||||
|
def test_run_train_classifier_task(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A superuser
|
||||||
|
WHEN:
|
||||||
|
- API call is made to run the train classifier task
|
||||||
|
THEN:
|
||||||
|
- The task is run
|
||||||
|
"""
|
||||||
|
mock_train_classifier = mock.Mock(return_value="Task started")
|
||||||
|
TasksViewSet.TASK_AND_ARGS_BY_NAME = {
|
||||||
|
PaperlessTask.TaskName.TRAIN_CLASSIFIER: (
|
||||||
|
mock_train_classifier,
|
||||||
|
{"scheduled": False},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
self.ENDPOINT + "run/",
|
||||||
|
{"task_name": PaperlessTask.TaskName.TRAIN_CLASSIFIER},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data, {"result": "Task started"})
|
||||||
|
mock_train_classifier.assert_called_once_with(scheduled=False)
|
||||||
|
|
||||||
|
# mock error
|
||||||
|
mock_train_classifier.reset_mock()
|
||||||
|
mock_train_classifier.side_effect = Exception("Error")
|
||||||
|
response = self.client.post(
|
||||||
|
self.ENDPOINT + "run/",
|
||||||
|
{"task_name": PaperlessTask.TaskName.TRAIN_CLASSIFIER},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
mock_train_classifier.assert_called_once_with(scheduled=False)
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.sanity_check")
|
||||||
|
def test_run_task_requires_superuser(self, mock_check_sanity):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A regular user
|
||||||
|
WHEN:
|
||||||
|
- API call is made to run a task
|
||||||
|
THEN:
|
||||||
|
- The task is not run
|
||||||
|
"""
|
||||||
|
regular_user = User.objects.create_user(username="test")
|
||||||
|
regular_user.user_permissions.add(*Permission.objects.all())
|
||||||
|
self.client.logout()
|
||||||
|
self.client.force_authenticate(user=regular_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.ENDPOINT + "run/",
|
||||||
|
{"task_name": PaperlessTask.TaskName.CHECK_SANITY},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
mock_check_sanity.assert_not_called()
|
||||||
|
@ -68,7 +68,7 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
|
|||||||
self.assertIsNotNone(task)
|
self.assertIsNotNone(task)
|
||||||
self.assertEqual(headers["id"], task.task_id)
|
self.assertEqual(headers["id"], task.task_id)
|
||||||
self.assertEqual("hello-999.pdf", task.task_file_name)
|
self.assertEqual("hello-999.pdf", task.task_file_name)
|
||||||
self.assertEqual("documents.tasks.consume_file", task.task_name)
|
self.assertEqual(PaperlessTask.TaskName.CONSUME_FILE, task.task_name)
|
||||||
self.assertEqual(1, task.owner_id)
|
self.assertEqual(1, task.owner_id)
|
||||||
self.assertEqual(celery.states.PENDING, task.status)
|
self.assertEqual(celery.states.PENDING, task.status)
|
||||||
|
|
||||||
|
@ -118,6 +118,19 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
|
|||||||
self.assertRaises(SanityCheckFailedException, tasks.sanity_check)
|
self.assertRaises(SanityCheckFailedException, tasks.sanity_check)
|
||||||
m.assert_called_once()
|
m.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.sanity_checker.check_sanity")
|
||||||
|
def test_sanity_check_error_no_raise(self, m):
|
||||||
|
messages = SanityCheckMessages()
|
||||||
|
messages.error(None, "Some error")
|
||||||
|
m.return_value = messages
|
||||||
|
# No exception should be raised
|
||||||
|
result = tasks.sanity_check(raise_on_error=False)
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
"Sanity check exited with errors. See log.",
|
||||||
|
)
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
@mock.patch("documents.tasks.sanity_checker.check_sanity")
|
@mock.patch("documents.tasks.sanity_checker.check_sanity")
|
||||||
def test_sanity_check_warning(self, m):
|
def test_sanity_check_warning(self, m):
|
||||||
messages = SanityCheckMessages()
|
messages = SanityCheckMessages()
|
||||||
|
@ -14,6 +14,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
|
from celery import states
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -103,6 +104,7 @@ from documents.filters import DocumentsOrderingFilter
|
|||||||
from documents.filters import DocumentTypeFilterSet
|
from documents.filters import DocumentTypeFilterSet
|
||||||
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
||||||
from documents.filters import ObjectOwnedPermissionsFilter
|
from documents.filters import ObjectOwnedPermissionsFilter
|
||||||
|
from documents.filters import PaperlessTaskFilterSet
|
||||||
from documents.filters import ShareLinkFilterSet
|
from documents.filters import ShareLinkFilterSet
|
||||||
from documents.filters import StoragePathFilterSet
|
from documents.filters import StoragePathFilterSet
|
||||||
from documents.filters import TagFilterSet
|
from documents.filters import TagFilterSet
|
||||||
@ -144,6 +146,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 RunTaskViewSerializer
|
||||||
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
|
||||||
@ -160,6 +163,9 @@ 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 index_optimize
|
||||||
|
from documents.tasks import sanity_check
|
||||||
|
from documents.tasks import train_classifier
|
||||||
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
|
||||||
@ -2276,16 +2282,27 @@ class RemoteVersionView(GenericAPIView):
|
|||||||
class TasksViewSet(ReadOnlyModelViewSet):
|
class TasksViewSet(ReadOnlyModelViewSet):
|
||||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
serializer_class = TasksViewSerializer
|
serializer_class = TasksViewSerializer
|
||||||
filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,)
|
filter_backends = (
|
||||||
|
DjangoFilterBackend,
|
||||||
|
OrderingFilter,
|
||||||
|
ObjectOwnedOrGrantedPermissionsFilter,
|
||||||
|
)
|
||||||
|
filterset_class = PaperlessTaskFilterSet
|
||||||
|
|
||||||
|
TASK_AND_ARGS_BY_NAME = {
|
||||||
|
PaperlessTask.TaskName.INDEX_OPTIMIZE: (index_optimize, {}),
|
||||||
|
PaperlessTask.TaskName.TRAIN_CLASSIFIER: (
|
||||||
|
train_classifier,
|
||||||
|
{"scheduled": False},
|
||||||
|
),
|
||||||
|
PaperlessTask.TaskName.CHECK_SANITY: (
|
||||||
|
sanity_check,
|
||||||
|
{"scheduled": False, "raise_on_error": False},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = (
|
queryset = PaperlessTask.objects.all().order_by("-date_created")
|
||||||
PaperlessTask.objects.filter(
|
|
||||||
acknowledged=False,
|
|
||||||
)
|
|
||||||
.order_by("date_created")
|
|
||||||
.reverse()
|
|
||||||
)
|
|
||||||
task_id = self.request.query_params.get("task_id")
|
task_id = self.request.query_params.get("task_id")
|
||||||
if task_id is not None:
|
if task_id is not None:
|
||||||
queryset = PaperlessTask.objects.filter(task_id=task_id)
|
queryset = PaperlessTask.objects.filter(task_id=task_id)
|
||||||
@ -2308,6 +2325,25 @@ class TasksViewSet(ReadOnlyModelViewSet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@action(methods=["post"], detail=False)
|
||||||
|
def run(self, request):
|
||||||
|
serializer = RunTaskViewSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
task_name = serializer.validated_data.get("task_name")
|
||||||
|
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return HttpResponseForbidden("Insufficient permissions")
|
||||||
|
|
||||||
|
try:
|
||||||
|
task_func, task_args = self.TASK_AND_ARGS_BY_NAME[task_name]
|
||||||
|
result = task_func(**task_args)
|
||||||
|
return Response({"result": result})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"An error occurred running task: {e!s}")
|
||||||
|
return HttpResponseServerError(
|
||||||
|
"Error running task, check logs for more detail.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ShareLinkViewSet(ModelViewSet, PassUserMixin):
|
class ShareLinkViewSet(ModelViewSet, PassUserMixin):
|
||||||
model = ShareLink
|
model = ShareLink
|
||||||
@ -2614,6 +2650,14 @@ class CustomFieldViewSet(ModelViewSet):
|
|||||||
"last_trained": serializers.DateTimeField(),
|
"last_trained": serializers.DateTimeField(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
"sanity_check": inline_serializer(
|
||||||
|
name="SanityCheck",
|
||||||
|
fields={
|
||||||
|
"status": serializers.CharField(),
|
||||||
|
"error": serializers.CharField(),
|
||||||
|
"last_run": serializers.DateTimeField(),
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -2674,13 +2718,20 @@ class SystemStatusView(PassUserMixin):
|
|||||||
)
|
)
|
||||||
redis_error = "Error connecting to redis, check logs for more detail."
|
redis_error = "Error connecting to redis, check logs for more detail."
|
||||||
|
|
||||||
|
celery_error = None
|
||||||
|
celery_url = None
|
||||||
try:
|
try:
|
||||||
celery_ping = celery_app.control.inspect().ping()
|
celery_ping = celery_app.control.inspect().ping()
|
||||||
first_worker_ping = celery_ping[next(iter(celery_ping.keys()))]
|
celery_url = next(iter(celery_ping.keys()))
|
||||||
|
first_worker_ping = celery_ping[celery_url]
|
||||||
if first_worker_ping["ok"] == "pong":
|
if first_worker_ping["ok"] == "pong":
|
||||||
celery_active = "OK"
|
celery_active = "OK"
|
||||||
except Exception:
|
except Exception as e:
|
||||||
celery_active = "ERROR"
|
celery_active = "ERROR"
|
||||||
|
logger.exception(
|
||||||
|
f"System status detected a possible problem while connecting to celery: {e}",
|
||||||
|
)
|
||||||
|
celery_error = "Error connecting to celery, check logs for more detail."
|
||||||
|
|
||||||
index_error = None
|
index_error = None
|
||||||
try:
|
try:
|
||||||
@ -2697,55 +2748,43 @@ class SystemStatusView(PassUserMixin):
|
|||||||
)
|
)
|
||||||
index_last_modified = None
|
index_last_modified = None
|
||||||
|
|
||||||
|
last_trained_task = (
|
||||||
|
PaperlessTask.objects.filter(
|
||||||
|
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||||
|
)
|
||||||
|
.order_by("-date_done")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
classifier_status = "OK"
|
||||||
classifier_error = None
|
classifier_error = None
|
||||||
classifier_status = None
|
if last_trained_task is None:
|
||||||
try:
|
classifier_status = "WARNING"
|
||||||
classifier = load_classifier(raise_exception=True)
|
classifier_error = "No classifier training tasks found"
|
||||||
if classifier is None:
|
elif last_trained_task and last_trained_task.status == states.FAILURE:
|
||||||
# Make sure classifier should exist
|
classifier_status = "ERROR"
|
||||||
docs_queryset = Document.objects.exclude(
|
classifier_error = last_trained_task.result
|
||||||
tags__is_inbox_tag=True,
|
classifier_last_trained = (
|
||||||
)
|
last_trained_task.date_done if last_trained_task else None
|
||||||
if (
|
)
|
||||||
docs_queryset.count() > 0
|
|
||||||
and (
|
last_sanity_check = (
|
||||||
Tag.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
PaperlessTask.objects.filter(
|
||||||
or DocumentType.objects.filter(
|
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||||
matching_algorithm=Tag.MATCH_AUTO,
|
|
||||||
).exists()
|
|
||||||
or Correspondent.objects.filter(
|
|
||||||
matching_algorithm=Tag.MATCH_AUTO,
|
|
||||||
).exists()
|
|
||||||
or StoragePath.objects.filter(
|
|
||||||
matching_algorithm=Tag.MATCH_AUTO,
|
|
||||||
).exists()
|
|
||||||
)
|
|
||||||
and not settings.MODEL_FILE.exists()
|
|
||||||
):
|
|
||||||
# if classifier file doesn't exist just classify as a warning
|
|
||||||
classifier_error = "Classifier file does not exist (yet). Re-training may be pending."
|
|
||||||
classifier_status = "WARNING"
|
|
||||||
raise FileNotFoundError(classifier_error)
|
|
||||||
classifier_status = "OK"
|
|
||||||
classifier_last_trained = (
|
|
||||||
make_aware(
|
|
||||||
datetime.fromtimestamp(classifier.get_last_checked()),
|
|
||||||
)
|
|
||||||
if settings.MODEL_FILE.exists()
|
|
||||||
and classifier.get_last_checked() is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
if classifier_status is None:
|
|
||||||
classifier_status = "ERROR"
|
|
||||||
classifier_last_trained = None
|
|
||||||
if classifier_error is None:
|
|
||||||
classifier_error = (
|
|
||||||
"Unable to load classifier, check logs for more detail."
|
|
||||||
)
|
|
||||||
logger.exception(
|
|
||||||
f"System status detected a possible problem while loading the classifier: {e}",
|
|
||||||
)
|
)
|
||||||
|
.order_by("-date_done")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
sanity_check_status = "OK"
|
||||||
|
sanity_check_error = None
|
||||||
|
if last_sanity_check is None:
|
||||||
|
sanity_check_status = "WARNING"
|
||||||
|
sanity_check_error = "No sanity check tasks found"
|
||||||
|
elif last_sanity_check and last_sanity_check.status == states.FAILURE:
|
||||||
|
sanity_check_status = "ERROR"
|
||||||
|
sanity_check_error = last_sanity_check.result
|
||||||
|
sanity_check_last_run = (
|
||||||
|
last_sanity_check.date_done if last_sanity_check else None
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -2773,12 +2812,17 @@ class SystemStatusView(PassUserMixin):
|
|||||||
"redis_status": redis_status,
|
"redis_status": redis_status,
|
||||||
"redis_error": redis_error,
|
"redis_error": redis_error,
|
||||||
"celery_status": celery_active,
|
"celery_status": celery_active,
|
||||||
|
"celery_url": celery_url,
|
||||||
|
"celery_error": celery_error,
|
||||||
"index_status": index_status,
|
"index_status": index_status,
|
||||||
"index_last_modified": index_last_modified,
|
"index_last_modified": index_last_modified,
|
||||||
"index_error": index_error,
|
"index_error": index_error,
|
||||||
"classifier_status": classifier_status,
|
"classifier_status": classifier_status,
|
||||||
"classifier_last_trained": classifier_last_trained,
|
"classifier_last_trained": classifier_last_trained,
|
||||||
"classifier_error": classifier_error,
|
"classifier_error": classifier_error,
|
||||||
|
"sanity_check_status": sanity_check_status,
|
||||||
|
"sanity_check_last_run": sanity_check_last_run,
|
||||||
|
"sanity_check_error": sanity_check_error,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-02-11 18:43-0800\n"
|
"POT-Creation-Date: 2025-02-25 11:07-0800\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@ -21,67 +21,67 @@ msgstr ""
|
|||||||
msgid "Documents"
|
msgid "Documents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:369
|
#: documents/filters.py:370
|
||||||
msgid "Value must be valid JSON."
|
msgid "Value must be valid JSON."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:388
|
#: documents/filters.py:389
|
||||||
msgid "Invalid custom field query expression"
|
msgid "Invalid custom field query expression"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:398
|
#: documents/filters.py:399
|
||||||
msgid "Invalid expression list. Must be nonempty."
|
msgid "Invalid expression list. Must be nonempty."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:419
|
#: documents/filters.py:420
|
||||||
msgid "Invalid logical operator {op!r}"
|
msgid "Invalid logical operator {op!r}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:433
|
#: documents/filters.py:434
|
||||||
msgid "Maximum number of query conditions exceeded."
|
msgid "Maximum number of query conditions exceeded."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:498
|
#: documents/filters.py:499
|
||||||
msgid "{name!r} is not a valid custom field."
|
msgid "{name!r} is not a valid custom field."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:535
|
#: documents/filters.py:536
|
||||||
msgid "{data_type} does not support query expr {expr!r}."
|
msgid "{data_type} does not support query expr {expr!r}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:643
|
#: documents/filters.py:644
|
||||||
msgid "Maximum nesting depth exceeded."
|
msgid "Maximum nesting depth exceeded."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:813
|
#: documents/filters.py:829
|
||||||
msgid "Custom field not found"
|
msgid "Custom field not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:41 documents/models.py:806
|
#: documents/models.py:41 documents/models.py:830
|
||||||
msgid "owner"
|
msgid "owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:58 documents/models.py:1017
|
#: documents/models.py:58 documents/models.py:1041
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:59 documents/models.py:1018
|
#: documents/models.py:59 documents/models.py:1042
|
||||||
msgid "Any word"
|
msgid "Any word"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:60 documents/models.py:1019
|
#: documents/models.py:60 documents/models.py:1043
|
||||||
msgid "All words"
|
msgid "All words"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:61 documents/models.py:1020
|
#: documents/models.py:61 documents/models.py:1044
|
||||||
msgid "Exact match"
|
msgid "Exact match"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:62 documents/models.py:1021
|
#: documents/models.py:62 documents/models.py:1045
|
||||||
msgid "Regular expression"
|
msgid "Regular expression"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:63 documents/models.py:1022
|
#: documents/models.py:63 documents/models.py:1046
|
||||||
msgid "Fuzzy word"
|
msgid "Fuzzy word"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -89,20 +89,20 @@ msgstr ""
|
|||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:67 documents/models.py:433 documents/models.py:1498
|
#: documents/models.py:67 documents/models.py:433 documents/models.py:1526
|
||||||
#: paperless_mail/models.py:23 paperless_mail/models.py:143
|
#: paperless_mail/models.py:23 paperless_mail/models.py:143
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:69 documents/models.py:1085
|
#: documents/models.py:69 documents/models.py:1110
|
||||||
msgid "match"
|
msgid "match"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:72 documents/models.py:1088
|
#: documents/models.py:72 documents/models.py:1113
|
||||||
msgid "matching algorithm"
|
msgid "matching algorithm"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:77 documents/models.py:1093
|
#: documents/models.py:77 documents/models.py:1118
|
||||||
msgid "is insensitive"
|
msgid "is insensitive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ msgstr ""
|
|||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:175 documents/models.py:720
|
#: documents/models.py:175 documents/models.py:744
|
||||||
msgid "content"
|
msgid "content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -206,8 +206,8 @@ msgstr ""
|
|||||||
msgid "The number of pages of the document."
|
msgid "The number of pages of the document."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:221 documents/models.py:401 documents/models.py:726
|
#: documents/models.py:221 documents/models.py:401 documents/models.py:750
|
||||||
#: documents/models.py:764 documents/models.py:835 documents/models.py:893
|
#: documents/models.py:788 documents/models.py:859 documents/models.py:917
|
||||||
msgid "created"
|
msgid "created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -255,8 +255,8 @@ msgstr ""
|
|||||||
msgid "The position of this document in your physical document archive."
|
msgid "The position of this document in your physical document archive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:295 documents/models.py:737 documents/models.py:791
|
#: documents/models.py:295 documents/models.py:761 documents/models.py:815
|
||||||
#: documents/models.py:1541
|
#: documents/models.py:1569
|
||||||
msgid "document"
|
msgid "document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -320,11 +320,11 @@ msgstr ""
|
|||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:420 documents/models.py:1037
|
#: documents/models.py:420 documents/models.py:1062
|
||||||
msgid "Created"
|
msgid "Created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:421 documents/models.py:1036
|
#: documents/models.py:421 documents/models.py:1061
|
||||||
msgid "Added"
|
msgid "Added"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -608,581 +608,621 @@ msgstr ""
|
|||||||
msgid "filter rules"
|
msgid "filter rules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:656
|
#: documents/models.py:654
|
||||||
msgid "Task ID"
|
msgid "Auto Task"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:657
|
#: documents/models.py:655
|
||||||
msgid "Celery ID for the Task that was run"
|
msgid "Scheduled Task"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:656
|
||||||
|
msgid "Manual Task"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:659
|
||||||
|
msgid "Consume File"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:660
|
||||||
|
msgid "Train Classifier"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:661
|
||||||
|
msgid "Check Sanity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:662
|
#: documents/models.py:662
|
||||||
|
msgid "Index Optimize"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:667
|
||||||
|
msgid "Task ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:668
|
||||||
|
msgid "Celery ID for the Task that was run"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:673
|
||||||
msgid "Acknowledged"
|
msgid "Acknowledged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:663
|
#: documents/models.py:674
|
||||||
msgid "If the task is acknowledged via the frontend or API"
|
msgid "If the task is acknowledged via the frontend or API"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:669
|
#: documents/models.py:680
|
||||||
msgid "Task Filename"
|
msgid "Task Filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:670
|
#: documents/models.py:681
|
||||||
msgid "Name of the file which the Task was run for"
|
msgid "Name of the file which the Task was run for"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:676
|
#: documents/models.py:688
|
||||||
msgid "Task Name"
|
msgid "Task Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:677
|
#: documents/models.py:689
|
||||||
msgid "Name of the Task which was run"
|
msgid "Name of the task that was run"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:684
|
|
||||||
msgid "Task State"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:685
|
|
||||||
msgid "Current state of the task being run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:690
|
|
||||||
msgid "Created DateTime"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:691
|
|
||||||
msgid "Datetime field when the task result was created in UTC"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:696
|
#: documents/models.py:696
|
||||||
msgid "Started DateTime"
|
msgid "Task State"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:697
|
#: documents/models.py:697
|
||||||
msgid "Datetime field when the task was started in UTC"
|
msgid "Current state of the task being run"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/models.py:702
|
|
||||||
msgid "Completed DateTime"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:703
|
#: documents/models.py:703
|
||||||
msgid "Datetime field when the task was completed in UTC"
|
msgid "Created DateTime"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:708
|
#: documents/models.py:704
|
||||||
msgid "Result Data"
|
msgid "Datetime field when the task result was created in UTC"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:710
|
#: documents/models.py:710
|
||||||
|
msgid "Started DateTime"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:711
|
||||||
|
msgid "Datetime field when the task was started in UTC"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:717
|
||||||
|
msgid "Completed DateTime"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:718
|
||||||
|
msgid "Datetime field when the task was completed in UTC"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:724
|
||||||
|
msgid "Result Data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:726
|
||||||
msgid "The data returned by the task"
|
msgid "The data returned by the task"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:722
|
#: documents/models.py:734
|
||||||
msgid "Note for the document"
|
msgid "Task Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:735
|
||||||
|
msgid "The type of task that was run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:746
|
#: documents/models.py:746
|
||||||
|
msgid "Note for the document"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:770
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:751
|
#: documents/models.py:775
|
||||||
msgid "note"
|
msgid "note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:752
|
#: documents/models.py:776
|
||||||
msgid "notes"
|
msgid "notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:760
|
#: documents/models.py:784
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:761
|
#: documents/models.py:785
|
||||||
msgid "Original"
|
msgid "Original"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:772 paperless_mail/models.py:75
|
#: documents/models.py:796 paperless_mail/models.py:75
|
||||||
msgid "expiration"
|
msgid "expiration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:779
|
#: documents/models.py:803
|
||||||
msgid "slug"
|
msgid "slug"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:811
|
#: documents/models.py:835
|
||||||
msgid "share link"
|
msgid "share link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:812
|
#: documents/models.py:836
|
||||||
msgid "share links"
|
msgid "share links"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:824
|
#: documents/models.py:848
|
||||||
msgid "String"
|
msgid "String"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:825
|
#: documents/models.py:849
|
||||||
msgid "URL"
|
msgid "URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:826
|
#: documents/models.py:850
|
||||||
msgid "Date"
|
msgid "Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:827
|
#: documents/models.py:851
|
||||||
msgid "Boolean"
|
msgid "Boolean"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:828
|
#: documents/models.py:852
|
||||||
msgid "Integer"
|
msgid "Integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:829
|
#: documents/models.py:853
|
||||||
msgid "Float"
|
msgid "Float"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:830
|
#: documents/models.py:854
|
||||||
msgid "Monetary"
|
msgid "Monetary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:831
|
#: documents/models.py:855
|
||||||
msgid "Document Link"
|
msgid "Document Link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:832
|
#: documents/models.py:856
|
||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:844
|
#: documents/models.py:868
|
||||||
msgid "data type"
|
msgid "data type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:851
|
#: documents/models.py:875
|
||||||
msgid "extra data"
|
msgid "extra data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:855
|
#: documents/models.py:879
|
||||||
msgid "Extra data for the custom field, such as select options"
|
msgid "Extra data for the custom field, such as select options"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:861
|
#: documents/models.py:885
|
||||||
msgid "custom field"
|
msgid "custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:862
|
#: documents/models.py:886
|
||||||
msgid "custom fields"
|
msgid "custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:959
|
#: documents/models.py:983
|
||||||
msgid "custom field instance"
|
msgid "custom field instance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:960
|
#: documents/models.py:984
|
||||||
msgid "custom field instances"
|
msgid "custom field instances"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1025
|
#: documents/models.py:1049
|
||||||
msgid "Consumption Started"
|
msgid "Consumption Started"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1026
|
#: documents/models.py:1050
|
||||||
msgid "Document Added"
|
msgid "Document Added"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1027
|
#: documents/models.py:1051
|
||||||
msgid "Document Updated"
|
msgid "Document Updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1028
|
#: documents/models.py:1052
|
||||||
msgid "Scheduled"
|
msgid "Scheduled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1031
|
#: documents/models.py:1055
|
||||||
msgid "Consume Folder"
|
msgid "Consume Folder"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1032
|
#: documents/models.py:1056
|
||||||
msgid "Api Upload"
|
msgid "Api Upload"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1033
|
#: documents/models.py:1057
|
||||||
msgid "Mail Fetch"
|
msgid "Mail Fetch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1038
|
#: documents/models.py:1058
|
||||||
|
msgid "Web UI"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:1063
|
||||||
msgid "Modified"
|
msgid "Modified"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1039
|
#: documents/models.py:1064
|
||||||
msgid "Custom Field"
|
msgid "Custom Field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1042
|
#: documents/models.py:1067
|
||||||
msgid "Workflow Trigger Type"
|
msgid "Workflow Trigger Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1054
|
#: documents/models.py:1079
|
||||||
msgid "filter path"
|
msgid "filter path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1059
|
#: documents/models.py:1084
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only consume documents with a path that matches this if specified. Wildcards "
|
"Only consume documents with a path that matches this if specified. Wildcards "
|
||||||
"specified as * are allowed. Case insensitive."
|
"specified as * are allowed. Case insensitive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1066
|
#: documents/models.py:1091
|
||||||
msgid "filter filename"
|
msgid "filter filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1071 paperless_mail/models.py:200
|
#: documents/models.py:1096 paperless_mail/models.py:200
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only consume documents which entirely match this filename if specified. "
|
"Only consume documents which entirely match this filename if specified. "
|
||||||
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1082
|
#: documents/models.py:1107
|
||||||
msgid "filter documents from this mail rule"
|
msgid "filter documents from this mail rule"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1098
|
#: documents/models.py:1123
|
||||||
msgid "has these tag(s)"
|
msgid "has these tag(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1106
|
#: documents/models.py:1131
|
||||||
msgid "has this document type"
|
msgid "has this document type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1114
|
#: documents/models.py:1139
|
||||||
msgid "has this correspondent"
|
msgid "has this correspondent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1118
|
#: documents/models.py:1143
|
||||||
msgid "schedule offset days"
|
msgid "schedule offset days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1121
|
#: documents/models.py:1146
|
||||||
msgid "The number of days to offset the schedule trigger by."
|
msgid "The number of days to offset the schedule trigger by."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1126
|
#: documents/models.py:1151
|
||||||
msgid "schedule is recurring"
|
msgid "schedule is recurring"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1129
|
#: documents/models.py:1154
|
||||||
msgid "If the schedule should be recurring."
|
msgid "If the schedule should be recurring."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1134
|
#: documents/models.py:1159
|
||||||
msgid "schedule recurring delay in days"
|
msgid "schedule recurring delay in days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1138
|
#: documents/models.py:1163
|
||||||
msgid "The number of days between recurring schedule triggers."
|
msgid "The number of days between recurring schedule triggers."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1143
|
#: documents/models.py:1168
|
||||||
msgid "schedule date field"
|
msgid "schedule date field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1148
|
#: documents/models.py:1173
|
||||||
msgid "The field to check for a schedule trigger."
|
msgid "The field to check for a schedule trigger."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1157
|
#: documents/models.py:1182
|
||||||
msgid "schedule date custom field"
|
msgid "schedule date custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1161
|
#: documents/models.py:1186
|
||||||
msgid "workflow trigger"
|
msgid "workflow trigger"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1162
|
#: documents/models.py:1187
|
||||||
msgid "workflow triggers"
|
msgid "workflow triggers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1170
|
#: documents/models.py:1195
|
||||||
msgid "email subject"
|
msgid "email subject"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1174
|
#: documents/models.py:1199
|
||||||
msgid ""
|
msgid ""
|
||||||
"The subject of the email, can include some placeholders, see documentation."
|
"The subject of the email, can include some placeholders, see documentation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1180
|
#: documents/models.py:1205
|
||||||
msgid "email body"
|
msgid "email body"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1183
|
#: documents/models.py:1208
|
||||||
msgid ""
|
msgid ""
|
||||||
"The body (message) of the email, can include some placeholders, see "
|
"The body (message) of the email, can include some placeholders, see "
|
||||||
"documentation."
|
"documentation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1189
|
#: documents/models.py:1214
|
||||||
msgid "emails to"
|
msgid "emails to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1192
|
#: documents/models.py:1217
|
||||||
msgid "The destination email addresses, comma separated."
|
msgid "The destination email addresses, comma separated."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1198
|
#: documents/models.py:1223
|
||||||
msgid "include document in email"
|
msgid "include document in email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1207
|
#: documents/models.py:1234
|
||||||
msgid "webhook url"
|
msgid "webhook url"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1209
|
#: documents/models.py:1237
|
||||||
msgid "The destination URL for the notification."
|
msgid "The destination URL for the notification."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1214
|
#: documents/models.py:1242
|
||||||
msgid "use parameters"
|
msgid "use parameters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1219
|
#: documents/models.py:1247
|
||||||
msgid "send as JSON"
|
msgid "send as JSON"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1223
|
#: documents/models.py:1251
|
||||||
msgid "webhook parameters"
|
msgid "webhook parameters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1226
|
#: documents/models.py:1254
|
||||||
msgid "The parameters to send with the webhook URL if body not used."
|
msgid "The parameters to send with the webhook URL if body not used."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1230
|
#: documents/models.py:1258
|
||||||
msgid "webhook body"
|
msgid "webhook body"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1233
|
#: documents/models.py:1261
|
||||||
msgid "The body to send with the webhook URL if parameters not used."
|
msgid "The body to send with the webhook URL if parameters not used."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1237
|
#: documents/models.py:1265
|
||||||
msgid "webhook headers"
|
msgid "webhook headers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1240
|
#: documents/models.py:1268
|
||||||
msgid "The headers to send with the webhook URL."
|
msgid "The headers to send with the webhook URL."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1245
|
#: documents/models.py:1273
|
||||||
msgid "include document in webhook"
|
msgid "include document in webhook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1256
|
#: documents/models.py:1284
|
||||||
msgid "Assignment"
|
msgid "Assignment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1260
|
#: documents/models.py:1288
|
||||||
msgid "Removal"
|
msgid "Removal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1264 documents/templates/account/password_reset.html:15
|
#: documents/models.py:1292 documents/templates/account/password_reset.html:15
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1268
|
#: documents/models.py:1296
|
||||||
msgid "Webhook"
|
msgid "Webhook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1272
|
#: documents/models.py:1300
|
||||||
msgid "Workflow Action Type"
|
msgid "Workflow Action Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1278
|
#: documents/models.py:1306
|
||||||
msgid "assign title"
|
msgid "assign title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1283
|
#: documents/models.py:1311
|
||||||
msgid ""
|
msgid ""
|
||||||
"Assign a document title, can include some placeholders, see documentation."
|
"Assign a document title, can include some placeholders, see documentation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1292 paperless_mail/models.py:274
|
#: documents/models.py:1320 paperless_mail/models.py:274
|
||||||
msgid "assign this tag"
|
msgid "assign this tag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1301 paperless_mail/models.py:282
|
#: documents/models.py:1329 paperless_mail/models.py:282
|
||||||
msgid "assign this document type"
|
msgid "assign this document type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1310 paperless_mail/models.py:296
|
#: documents/models.py:1338 paperless_mail/models.py:296
|
||||||
msgid "assign this correspondent"
|
msgid "assign this correspondent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1319
|
#: documents/models.py:1347
|
||||||
msgid "assign this storage path"
|
msgid "assign this storage path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1328
|
#: documents/models.py:1356
|
||||||
msgid "assign this owner"
|
msgid "assign this owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1335
|
#: documents/models.py:1363
|
||||||
msgid "grant view permissions to these users"
|
msgid "grant view permissions to these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1342
|
#: documents/models.py:1370
|
||||||
msgid "grant view permissions to these groups"
|
msgid "grant view permissions to these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1349
|
#: documents/models.py:1377
|
||||||
msgid "grant change permissions to these users"
|
msgid "grant change permissions to these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1356
|
#: documents/models.py:1384
|
||||||
msgid "grant change permissions to these groups"
|
msgid "grant change permissions to these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1363
|
#: documents/models.py:1391
|
||||||
msgid "assign these custom fields"
|
msgid "assign these custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1370
|
#: documents/models.py:1398
|
||||||
msgid "remove these tag(s)"
|
msgid "remove these tag(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1375
|
#: documents/models.py:1403
|
||||||
msgid "remove all tags"
|
msgid "remove all tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1382
|
#: documents/models.py:1410
|
||||||
msgid "remove these document type(s)"
|
msgid "remove these document type(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1387
|
#: documents/models.py:1415
|
||||||
msgid "remove all document types"
|
msgid "remove all document types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1394
|
#: documents/models.py:1422
|
||||||
msgid "remove these correspondent(s)"
|
msgid "remove these correspondent(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1399
|
#: documents/models.py:1427
|
||||||
msgid "remove all correspondents"
|
msgid "remove all correspondents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1406
|
#: documents/models.py:1434
|
||||||
msgid "remove these storage path(s)"
|
msgid "remove these storage path(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1411
|
#: documents/models.py:1439
|
||||||
msgid "remove all storage paths"
|
msgid "remove all storage paths"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1418
|
#: documents/models.py:1446
|
||||||
msgid "remove these owner(s)"
|
msgid "remove these owner(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1423
|
#: documents/models.py:1451
|
||||||
msgid "remove all owners"
|
msgid "remove all owners"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1430
|
#: documents/models.py:1458
|
||||||
msgid "remove view permissions for these users"
|
msgid "remove view permissions for these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1437
|
#: documents/models.py:1465
|
||||||
msgid "remove view permissions for these groups"
|
msgid "remove view permissions for these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1444
|
#: documents/models.py:1472
|
||||||
msgid "remove change permissions for these users"
|
msgid "remove change permissions for these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1451
|
#: documents/models.py:1479
|
||||||
msgid "remove change permissions for these groups"
|
msgid "remove change permissions for these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1456
|
#: documents/models.py:1484
|
||||||
msgid "remove all permissions"
|
msgid "remove all permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1463
|
#: documents/models.py:1491
|
||||||
msgid "remove these custom fields"
|
msgid "remove these custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1468
|
#: documents/models.py:1496
|
||||||
msgid "remove all custom fields"
|
msgid "remove all custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1477
|
#: documents/models.py:1505
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1486
|
#: documents/models.py:1514
|
||||||
msgid "webhook"
|
msgid "webhook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1490
|
#: documents/models.py:1518
|
||||||
msgid "workflow action"
|
msgid "workflow action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1491
|
#: documents/models.py:1519
|
||||||
msgid "workflow actions"
|
msgid "workflow actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1500 paperless_mail/models.py:145
|
#: documents/models.py:1528 paperless_mail/models.py:145
|
||||||
msgid "order"
|
msgid "order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1506
|
#: documents/models.py:1534
|
||||||
msgid "triggers"
|
msgid "triggers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1513
|
#: documents/models.py:1541
|
||||||
msgid "actions"
|
msgid "actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1516 paperless_mail/models.py:154
|
#: documents/models.py:1544 paperless_mail/models.py:154
|
||||||
msgid "enabled"
|
msgid "enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1527
|
#: documents/models.py:1555
|
||||||
msgid "workflow"
|
msgid "workflow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1531
|
#: documents/models.py:1559
|
||||||
msgid "workflow trigger type"
|
msgid "workflow trigger type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1545
|
#: documents/models.py:1573
|
||||||
msgid "date run"
|
msgid "date run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1551
|
#: documents/models.py:1579
|
||||||
msgid "workflow run"
|
msgid "workflow run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1552
|
#: documents/models.py:1580
|
||||||
msgid "workflow runs"
|
msgid "workflow runs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:127
|
#: documents/serialisers.py:128
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Invalid regular expression: %(error)s"
|
msgid "Invalid regular expression: %(error)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:553
|
#: documents/serialisers.py:554
|
||||||
msgid "Invalid color."
|
msgid "Invalid color."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:1554
|
#: documents/serialisers.py:1570
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "File type %(type)s not supported"
|
msgid "File type %(type)s not supported"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:1643
|
#: documents/serialisers.py:1659
|
||||||
msgid "Invalid variable detected."
|
msgid "Invalid variable detected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1402,17 +1442,23 @@ msgstr ""
|
|||||||
msgid "As a final step, please complete the following form:"
|
msgid "As a final step, please complete the following form:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/validators.py:17
|
#: documents/validators.py:24
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unable to parse URI {value}, missing scheme"
|
msgid "Unable to parse URI {value}, missing scheme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/validators.py:22
|
#: documents/validators.py:29
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unable to parse URI {value}, missing net location or path"
|
msgid "Unable to parse URI {value}, missing net location or path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/validators.py:27
|
#: documents/validators.py:36
|
||||||
|
msgid ""
|
||||||
|
"URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '."
|
||||||
|
"join(allowed_schemes)}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/validators.py:45
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Unable to parse URI {value}"
|
msgid "Unable to parse URI {value}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1701,7 +1747,7 @@ msgstr ""
|
|||||||
msgid "Chinese Traditional"
|
msgid "Chinese Traditional"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/urls.py:364
|
#: paperless/urls.py:369
|
||||||
msgid "Paperless-ngx administration"
|
msgid "Paperless-ngx administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user