mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Feature: document history (audit log UI) (#6388)
This commit is contained in:
		
							
								
								
									
										8
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -479,12 +479,12 @@ | |||||||
|         }, |         }, | ||||||
|         "django-auditlog": { |         "django-auditlog": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7", |                 "sha256:92db1cf4a51ceca5c26b3ff46997d9e3305a02da1bd435e2efb5b8b6d300ce1f", | ||||||
|                 "sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532" |                 "sha256:9de49f80a4911135d136017123cd73461f869b4947eec14d5e76db4b88182f3f" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.3.0" |             "version": "==3.0.0" | ||||||
|         }, |         }, | ||||||
|         "django-celery-results": { |         "django-celery-results": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|   | |||||||
| @@ -140,6 +140,7 @@ document. Paperless only reports PDF metadata at this point. | |||||||
|  |  | ||||||
| - `/api/documents/<id>/notes/`: Retrieve notes for a document. | - `/api/documents/<id>/notes/`: Retrieve notes for a document. | ||||||
| - `/api/documents/<id>/share_links/`: Retrieve share links for a document. | - `/api/documents/<id>/share_links/`: Retrieve share links for a document. | ||||||
|  | - `/api/documents/<id>/history/`: Retrieve history of changes for a document. | ||||||
|  |  | ||||||
| ## Authorization | ## Authorization | ||||||
|  |  | ||||||
|   | |||||||
| @@ -472,6 +472,12 @@ Paperless-ngx supports 3 basic editing operations for PDFs (these operations can | |||||||
|  |  | ||||||
|     Note that rotation alters the Paperless-ngx _original_ file, which would, for example, invalidate a digital signature. |     Note that rotation alters the Paperless-ngx _original_ file, which would, for example, invalidate a digital signature. | ||||||
|  |  | ||||||
|  | ## Document History | ||||||
|  |  | ||||||
|  | As of version 2.7, Paperless-ngx automatically records all changes to a document and records this in an audit log. The feature requires [`PAPERLESS_AUDIT_LOG_ENABLED`](configuration.md#PAPERLESS_AUDIT_LOG_ENABLED) be enabled, which it is by default as of version 2.7. | ||||||
|  | Changes to documents are visible under the "History" tab. Note that certain changes such as those made by workflows, record the 'actor' | ||||||
|  | as "System". | ||||||
|  |  | ||||||
| ## Best practices {#basic-searching} | ## Best practices {#basic-searching} | ||||||
|  |  | ||||||
| Paperless offers a couple tools that help you organize your document | Paperless offers a couple tools that help you organize your document | ||||||
|   | |||||||
| @@ -424,6 +424,10 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> |           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> | ||||||
|           <context context-type="linenumber">22</context> |           <context context-type="linenumber">22</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/document-history/document-history.component.html</context> | ||||||
|  |           <context context-type="linenumber">35</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7991430199894172363" datatype="html"> |       <trans-unit id="7991430199894172363" datatype="html"> | ||||||
|         <source>Read the documentation about this setting</source> |         <source>Read the documentation about this setting</source> | ||||||
| @@ -447,7 +451,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">322</context> |           <context context-type="linenumber">333</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3768927257183755959" datatype="html"> |       <trans-unit id="3768927257183755959" datatype="html"> | ||||||
| @@ -506,7 +510,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">314</context> |           <context context-type="linenumber">325</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context> | ||||||
| @@ -636,7 +640,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">331</context> |           <context context-type="linenumber">342</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> | ||||||
| @@ -947,7 +951,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">32</context> |           <context context-type="linenumber">33</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="293524471897878391" datatype="html"> |       <trans-unit id="293524471897878391" datatype="html"> | ||||||
| @@ -977,7 +981,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">290</context> |           <context context-type="linenumber">301</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> | ||||||
| @@ -1682,7 +1686,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">29</context> |           <context context-type="linenumber">30</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5968132631442328843" datatype="html"> |       <trans-unit id="5968132631442328843" datatype="html"> | ||||||
| @@ -2032,7 +2036,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">768</context> |           <context context-type="linenumber">769</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -2075,15 +2079,15 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">770</context> |           <context context-type="linenumber">771</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1052</context> |           <context context-type="linenumber">1064</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1090</context> |           <context context-type="linenumber">1102</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -4321,7 +4325,7 @@ | |||||||
|         <source>Inherited from group</source> |         <source>Inherited from group</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.ts</context> |           <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.ts</context> | ||||||
|           <context context-type="linenumber">61</context> |           <context context-type="linenumber">63</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6418218602775540217" datatype="html"> |       <trans-unit id="6418218602775540217" datatype="html"> | ||||||
| @@ -4822,7 +4826,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">27</context> |           <context context-type="linenumber">28</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2691296884221415710" datatype="html"> |       <trans-unit id="2691296884221415710" datatype="html"> | ||||||
| @@ -4849,7 +4853,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">26</context> |           <context context-type="linenumber">27</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8911158217491828773" datatype="html"> |       <trans-unit id="8911158217491828773" datatype="html"> | ||||||
| @@ -5119,7 +5123,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1108</context> |           <context context-type="linenumber">1120</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context> |           <context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context> | ||||||
| @@ -5174,7 +5178,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">28</context> |           <context context-type="linenumber">29</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2091353339965748767" datatype="html"> |       <trans-unit id="2091353339965748767" datatype="html"> | ||||||
| @@ -5312,103 +5316,110 @@ | |||||||
|           <context context-type="linenumber">279,282</context> |           <context context-type="linenumber">279,282</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="186236568870281953" datatype="html"> | ||||||
|  |         <source>History</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|  |           <context context-type="linenumber">290</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="5129524307369213584" datatype="html"> |       <trans-unit id="5129524307369213584" datatype="html"> | ||||||
|         <source>Save & next</source> |         <source>Save & next</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">316</context> |           <context context-type="linenumber">327</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4910102545766233758" datatype="html"> |       <trans-unit id="4910102545766233758" datatype="html"> | ||||||
|         <source>Save & close</source> |         <source>Save & close</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">319</context> |           <context context-type="linenumber">330</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8191371354890763172" datatype="html"> |       <trans-unit id="8191371354890763172" datatype="html"> | ||||||
|         <source>Enter Password</source> |         <source>Enter Password</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> | ||||||
|           <context context-type="linenumber">370</context> |           <context context-type="linenumber">381</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2218903673684131427" datatype="html"> |       <trans-unit id="2218903673684131427" datatype="html"> | ||||||
|         <source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source> |         <source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">327,329</context> |           <context context-type="linenumber">328,330</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3200733026060976258" datatype="html"> |       <trans-unit id="3200733026060976258" datatype="html"> | ||||||
|         <source>Document changes detected</source> |         <source>Document changes detected</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">350</context> |           <context context-type="linenumber">351</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2887155916749964" datatype="html"> |       <trans-unit id="2887155916749964" datatype="html"> | ||||||
|         <source>The version of this document in your browser session appears older than the existing version.</source> |         <source>The version of this document in your browser session appears older than the existing version.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">351</context> |           <context context-type="linenumber">352</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="237142428785956348" datatype="html"> |       <trans-unit id="237142428785956348" datatype="html"> | ||||||
|         <source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source> |         <source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">352</context> |           <context context-type="linenumber">353</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8720977247725652816" datatype="html"> |       <trans-unit id="8720977247725652816" datatype="html"> | ||||||
|         <source>Ok</source> |         <source>Ok</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">354</context> |           <context context-type="linenumber">355</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5758784066858623886" datatype="html"> |       <trans-unit id="5758784066858623886" datatype="html"> | ||||||
|         <source>Error retrieving metadata</source> |         <source>Error retrieving metadata</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">494</context> |           <context context-type="linenumber">495</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3456881259945295697" datatype="html"> |       <trans-unit id="3456881259945295697" datatype="html"> | ||||||
|         <source>Error retrieving suggestions.</source> |         <source>Error retrieving suggestions.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">519</context> |           <context context-type="linenumber">520</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8348337312757497317" datatype="html"> |       <trans-unit id="8348337312757497317" datatype="html"> | ||||||
|         <source>Document saved successfully.</source> |         <source>Document saved successfully.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">640</context> |           <context context-type="linenumber">641</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">651</context> |           <context context-type="linenumber">652</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="448882439049417053" datatype="html"> |       <trans-unit id="448882439049417053" datatype="html"> | ||||||
|         <source>Error saving document</source> |         <source>Error saving document</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">655</context> |           <context context-type="linenumber">656</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">696</context> |           <context context-type="linenumber">697</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="9021887951960049161" datatype="html"> |       <trans-unit id="9021887951960049161" datatype="html"> | ||||||
|         <source>Confirm delete</source> |         <source>Confirm delete</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">723</context> |           <context context-type="linenumber">724</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> |           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> | ||||||
| @@ -5423,35 +5434,35 @@ | |||||||
|         <source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source> |         <source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">724</context> |           <context context-type="linenumber">725</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6691075929777935948" datatype="html"> |       <trans-unit id="6691075929777935948" datatype="html"> | ||||||
|         <source>The files for this document will be deleted permanently. This operation cannot be undone.</source> |         <source>The files for this document will be deleted permanently. This operation cannot be undone.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">725</context> |           <context context-type="linenumber">726</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="719892092227206532" datatype="html"> |       <trans-unit id="719892092227206532" datatype="html"> | ||||||
|         <source>Delete document</source> |         <source>Delete document</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">727</context> |           <context context-type="linenumber">728</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7295637485862454066" datatype="html"> |       <trans-unit id="7295637485862454066" datatype="html"> | ||||||
|         <source>Error deleting document</source> |         <source>Error deleting document</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">746</context> |           <context context-type="linenumber">747</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7362691899087997122" datatype="html"> |       <trans-unit id="7362691899087997122" datatype="html"> | ||||||
|         <source>Redo OCR confirm</source> |         <source>Redo OCR confirm</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">766</context> |           <context context-type="linenumber">767</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -5462,63 +5473,63 @@ | |||||||
|         <source>This operation will permanently redo OCR for this document.</source> |         <source>This operation will permanently redo OCR for this document.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">767</context> |           <context context-type="linenumber">768</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5729001209753056399" datatype="html"> |       <trans-unit id="5729001209753056399" datatype="html"> | ||||||
|         <source>Redo OCR operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source> |         <source>Redo OCR operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">778</context> |           <context context-type="linenumber">779</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4409560272830824468" datatype="html"> |       <trans-unit id="4409560272830824468" datatype="html"> | ||||||
|         <source>Error executing operation</source> |         <source>Error executing operation</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">789</context> |           <context context-type="linenumber">790</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4458954481601077369" datatype="html"> |       <trans-unit id="4458954481601077369" datatype="html"> | ||||||
|         <source>Page Fit</source> |         <source>Page Fit</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">858</context> |           <context context-type="linenumber">859</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1217563727923422413" datatype="html"> |       <trans-unit id="1217563727923422413" datatype="html"> | ||||||
|         <source>Split confirm</source> |         <source>Split confirm</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1050</context> |           <context context-type="linenumber">1062</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2805304563009985503" datatype="html"> |       <trans-unit id="2805304563009985503" datatype="html"> | ||||||
|         <source>This operation will split the selected document(s) into new documents.</source> |         <source>This operation will split the selected document(s) into new documents.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1051</context> |           <context context-type="linenumber">1063</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4158171846914923744" datatype="html"> |       <trans-unit id="4158171846914923744" datatype="html"> | ||||||
|         <source>Split operation will begin in the background.</source> |         <source>Split operation will begin in the background.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1066</context> |           <context context-type="linenumber">1078</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3235014591864339926" datatype="html"> |       <trans-unit id="3235014591864339926" datatype="html"> | ||||||
|         <source>Error executing split operation</source> |         <source>Error executing split operation</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1075</context> |           <context context-type="linenumber">1087</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6555329262222566158" datatype="html"> |       <trans-unit id="6555329262222566158" datatype="html"> | ||||||
|         <source>Rotate confirm</source> |         <source>Rotate confirm</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1087</context> |           <context context-type="linenumber">1099</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -5529,14 +5540,14 @@ | |||||||
|         <source>This operation will permanently rotate the original version of the current document.</source> |         <source>This operation will permanently rotate the original version of the current document.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1088</context> |           <context context-type="linenumber">1100</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4233432423256408453" datatype="html"> |       <trans-unit id="4233432423256408453" datatype="html"> | ||||||
|         <source>This will alter the original copy.</source> |         <source>This will alter the original copy.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1089</context> |           <context context-type="linenumber">1101</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -5547,14 +5558,21 @@ | |||||||
|         <source>Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.</source> |         <source>Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1105</context> |           <context context-type="linenumber">1117</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2962674215361798818" datatype="html"> |       <trans-unit id="2962674215361798818" datatype="html"> | ||||||
|         <source>Error executing rotate operation</source> |         <source>Error executing rotate operation</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1117</context> |           <context context-type="linenumber">1129</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="4958946940233632319" datatype="html"> | ||||||
|  |         <source>No entries found.</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/document-history/document-history.component.html</context> | ||||||
|  |           <context context-type="linenumber">10</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6857598786757174736" datatype="html"> |       <trans-unit id="6857598786757174736" datatype="html"> | ||||||
| @@ -6094,7 +6112,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">25</context> |           <context context-type="linenumber">26</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6954625430271090777" datatype="html"> |       <trans-unit id="6954625430271090777" datatype="html"> | ||||||
| @@ -6126,7 +6144,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">33</context> |           <context context-type="linenumber">34</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3557446856808034218" datatype="html"> |       <trans-unit id="3557446856808034218" datatype="html"> | ||||||
| @@ -6176,7 +6194,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">30</context> |           <context context-type="linenumber">31</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2179847500064178686" datatype="html"> |       <trans-unit id="2179847500064178686" datatype="html"> | ||||||
| @@ -7370,6 +7388,97 @@ | |||||||
|           <context context-type="linenumber">36</context> |           <context context-type="linenumber">36</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="4272436583644511364" datatype="html"> | ||||||
|  |         <source>Just now</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">39</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="8456127468852940807" datatype="html"> | ||||||
|  |         <source>year ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">42</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="963494111451627204" datatype="html"> | ||||||
|  |         <source>years ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">43</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="1919405338795657780" datatype="html"> | ||||||
|  |         <source>month ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">47</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="6041340836190906216" datatype="html"> | ||||||
|  |         <source>months ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">48</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="4072659649620334828" datatype="html"> | ||||||
|  |         <source>week ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">52</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="2871318661796659216" datatype="html"> | ||||||
|  |         <source>weeks ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">53</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="1328378419272652134" datatype="html"> | ||||||
|  |         <source>day ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">57</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="5620397708418210833" datatype="html"> | ||||||
|  |         <source>days ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">58</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="4259498317457105735" datatype="html"> | ||||||
|  |         <source>hour ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">62</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="7576594819545407052" datatype="html"> | ||||||
|  |         <source>hours ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">63</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="4063456036422970205" datatype="html"> | ||||||
|  |         <source>minute ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">67</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="6906829094715901970" datatype="html"> | ||||||
|  |         <source>minutes ago</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context> | ||||||
|  |           <context context-type="linenumber">68</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="7536524521722799066" datatype="html"> |       <trans-unit id="7536524521722799066" datatype="html"> | ||||||
|         <source>(no title)</source> |         <source>(no title)</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
| @@ -7532,14 +7641,14 @@ | |||||||
|         <source>Modified</source> |         <source>Modified</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">31</context> |           <context context-type="linenumber">32</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4460262093225954455" datatype="html"> |       <trans-unit id="4460262093225954455" datatype="html"> | ||||||
|         <source>Search score</source> |         <source>Search score</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
|           <context context-type="linenumber">40</context> |           <context context-type="linenumber">41</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note> |         <note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|   | |||||||
| @@ -119,6 +119,7 @@ import { NgxFilesizeModule } from 'ngx-filesize' | |||||||
| import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' | import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' | ||||||
| import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' | import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' | ||||||
| import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' | import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' | ||||||
|  | import { DocumentHistoryComponent } from './components/document-history/document-history.component' | ||||||
| import { | import { | ||||||
|   airplane, |   airplane, | ||||||
|   archive, |   archive, | ||||||
| @@ -472,6 +473,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     RotateConfirmDialogComponent, |     RotateConfirmDialogComponent, | ||||||
|     MergeConfirmDialogComponent, |     MergeConfirmDialogComponent, | ||||||
|     SplitConfirmDialogComponent, |     SplitConfirmDialogComponent, | ||||||
|  |     DocumentHistoryComponent, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
|   | |||||||
| @@ -9,17 +9,17 @@ | |||||||
|       <div class="col" i18n>Delete</div> |       <div class="col" i18n>Delete</div> | ||||||
|       <div class="col" i18n>View</div> |       <div class="col" i18n>View</div> | ||||||
|     </li> |     </li> | ||||||
|     @for (type of PermissionType | keyvalue; track type) { |     @for (type of allowedTypes; track type) { | ||||||
|       <li class="list-group-item d-flex" [formGroupName]="type.key"> |       <li class="list-group-item d-flex" [formGroupName]="type"> | ||||||
|         <div class="col-3">{{type.key}}:</div> |         <div class="col-3">{{type}}:</div> | ||||||
|         <div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" placement="left" triggers="mouseenter:mouseleave"> |         <div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type)" placement="left" triggers="mouseenter:mouseleave"> | ||||||
|           <input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key) || isInherited(type.key)" [attr.disabled]="disabled || isInherited(type.key) ? true : null"> |           <input type="checkbox" class="form-check-input" id="{{type}}_all" (change)="toggleAll($event, type)" [checked]="typesWithAllActions.has(type) || isInherited(type)" [attr.disabled]="disabled || isInherited(type) ? true : null"> | ||||||
|           <label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label> |           <label class="form-check-label visually-hidden" for="{{type}}_all" i18n>All</label> | ||||||
|         </div> |         </div> | ||||||
|         @for (action of PermissionAction | keyvalue; track action) { |         @for (action of PermissionAction | keyvalue; track action) { | ||||||
|           <div class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key, action.key)" placement="left" triggers="mouseenter:mouseleave"> |           <div class="col form-check form-check-inline" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type, action.key)" placement="left" triggers="mouseenter:mouseleave"> | ||||||
|             <input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}"> |             <input type="checkbox" class="form-check-input" id="{{type}}_{{action.key}}" formControlName="{{action.key}}"> | ||||||
|             <label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label> |             <label class="form-check-label visually-hidden" for="{{type}}_{{action.key}}" i18n>{{action.key}}</label> | ||||||
|           </div> |           </div> | ||||||
|         } |         } | ||||||
|       </li> |       </li> | ||||||
|   | |||||||
| @@ -12,6 +12,9 @@ import { | |||||||
| } from 'src/app/services/permissions.service' | } from 'src/app/services/permissions.service' | ||||||
| import { By } from '@angular/platform-browser' | import { By } from '@angular/platform-browser' | ||||||
| import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | ||||||
|  | import { SettingsService } from 'src/app/services/settings.service' | ||||||
|  | import { SETTINGS_KEYS } from 'src/app/data/ui-settings' | ||||||
|  | import { HttpClientTestingModule } from '@angular/common/http/testing' | ||||||
|  |  | ||||||
| const permissions = [ | const permissions = [ | ||||||
|   'add_document', |   'add_document', | ||||||
| @@ -28,6 +31,7 @@ describe('PermissionsSelectComponent', () => { | |||||||
|   let component: PermissionsSelectComponent |   let component: PermissionsSelectComponent | ||||||
|   let fixture: ComponentFixture<PermissionsSelectComponent> |   let fixture: ComponentFixture<PermissionsSelectComponent> | ||||||
|   let permissionsChangeResult: Permissions |   let permissionsChangeResult: Permissions | ||||||
|  |   let settingsService: SettingsService | ||||||
|  |  | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     TestBed.configureTestingModule({ |     TestBed.configureTestingModule({ | ||||||
| @@ -38,9 +42,11 @@ describe('PermissionsSelectComponent', () => { | |||||||
|         ReactiveFormsModule, |         ReactiveFormsModule, | ||||||
|         NgbModule, |         NgbModule, | ||||||
|         NgxBootstrapIconsModule.pick(allIcons), |         NgxBootstrapIconsModule.pick(allIcons), | ||||||
|  |         HttpClientTestingModule, | ||||||
|       ], |       ], | ||||||
|     }).compileComponents() |     }).compileComponents() | ||||||
|  |  | ||||||
|  |     settingsService = TestBed.inject(SettingsService) | ||||||
|     fixture = TestBed.createComponent(PermissionsSelectComponent) |     fixture = TestBed.createComponent(PermissionsSelectComponent) | ||||||
|     fixture.debugElement.injector.get(NG_VALUE_ACCESSOR) |     fixture.debugElement.injector.get(NG_VALUE_ACCESSOR) | ||||||
|     component = fixture.componentInstance |     component = fixture.componentInstance | ||||||
| @@ -99,4 +105,11 @@ describe('PermissionsSelectComponent', () => { | |||||||
|     const input2 = fixture.debugElement.query(By.css('input#Tag_Change')) |     const input2 = fixture.debugElement.query(By.css('input#Tag_Change')) | ||||||
|     expect(input2.nativeElement.disabled).toBeTruthy() |     expect(input2.nativeElement.disabled).toBeTruthy() | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   it('should exclude history permissions if disabled', () => { | ||||||
|  |     settingsService.set(SETTINGS_KEYS.AUDITLOG_ENABLED, false) | ||||||
|  |     fixture = TestBed.createComponent(PermissionsSelectComponent) | ||||||
|  |     component = fixture.componentInstance | ||||||
|  |     expect(component.allowedTypes).not.toContain('History') | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import { | |||||||
|   PermissionType, |   PermissionType, | ||||||
| } from 'src/app/services/permissions.service' | } from 'src/app/services/permissions.service' | ||||||
| import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' | import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' | ||||||
|  | import { SettingsService } from 'src/app/services/settings.service' | ||||||
|  | import { SETTINGS_KEYS } from 'src/app/data/ui-settings' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   providers: [ |   providers: [ | ||||||
| @@ -60,15 +62,23 @@ export class PermissionsSelectComponent | |||||||
|  |  | ||||||
|   inheritedWarning: string = $localize`Inherited from group` |   inheritedWarning: string = $localize`Inherited from group` | ||||||
|  |  | ||||||
|   constructor(private readonly permissionsService: PermissionsService) { |   public allowedTypes = Object.keys(PermissionType) | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     private readonly permissionsService: PermissionsService, | ||||||
|  |     private readonly settingsService: SettingsService | ||||||
|  |   ) { | ||||||
|     super() |     super() | ||||||
|     for (const type in PermissionType) { |     if (!this.settingsService.get(SETTINGS_KEYS.AUDITLOG_ENABLED)) { | ||||||
|  |       this.allowedTypes.splice(this.allowedTypes.indexOf('History'), 1) | ||||||
|  |     } | ||||||
|  |     this.allowedTypes.forEach((type) => { | ||||||
|       const control = new FormGroup({}) |       const control = new FormGroup({}) | ||||||
|       for (const action in PermissionAction) { |       for (const action in PermissionAction) { | ||||||
|         control.addControl(action, new FormControl(null)) |         control.addControl(action, new FormControl(null)) | ||||||
|       } |       } | ||||||
|       this.form.addControl(type, control) |       this.form.addControl(type, control) | ||||||
|     } |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   writeValue(permissions: string[]): void { |   writeValue(permissions: string[]): void { | ||||||
| @@ -92,7 +102,7 @@ export class PermissionsSelectComponent | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|     Object.keys(PermissionType).forEach((type) => { |     this.allowedTypes.forEach((type) => { | ||||||
|       if ( |       if ( | ||||||
|         Object.values(this.form.get(type).value).every((val) => val == true) |         Object.values(this.form.get(type).value).every((val) => val == true) | ||||||
|       ) { |       ) { | ||||||
| @@ -191,7 +201,7 @@ export class PermissionsSelectComponent | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   updateDisabledStates() { |   updateDisabledStates() { | ||||||
|     for (const type in PermissionType) { |     this.allowedTypes.forEach((type) => { | ||||||
|       const control = this.form.get(type) |       const control = this.form.get(type) | ||||||
|       let actionControl: AbstractControl |       let actionControl: AbstractControl | ||||||
|       for (const action in PermissionAction) { |       for (const action in PermissionAction) { | ||||||
| @@ -200,6 +210,6 @@ export class PermissionsSelectComponent | |||||||
|           ? actionControl.disable() |           ? actionControl.disable() | ||||||
|           : actionControl.enable() |           : actionControl.enable() | ||||||
|       } |       } | ||||||
|     } |     }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -285,6 +285,17 @@ | |||||||
|           </li> |           </li> | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         @if (historyEnabled) { | ||||||
|  |           <li [ngbNavItem]="DocumentDetailNavIDs.History"> | ||||||
|  |             <a ngbNavLink i18n>History</a> | ||||||
|  |             <ng-template ngbNavContent> | ||||||
|  |               <div class="mb-3"> | ||||||
|  |                 <pngx-document-history [documentId]="documentId"></pngx-document-history> | ||||||
|  |               </div> | ||||||
|  |             </ng-template> | ||||||
|  |           </li> | ||||||
|  |         } | ||||||
|  |  | ||||||
|         @if (showPermissions) { |         @if (showPermissions) { | ||||||
|           <li [ngbNavItem]="DocumentDetailNavIDs.Permissions"> |           <li [ngbNavItem]="DocumentDetailNavIDs.Permissions"> | ||||||
|             <a ngbNavLink i18n>Permissions</a> |             <a ngbNavLink i18n>Permissions</a> | ||||||
|   | |||||||
| @@ -77,6 +77,7 @@ enum DocumentDetailNavIDs { | |||||||
|   Preview = 4, |   Preview = 4, | ||||||
|   Notes = 5, |   Notes = 5, | ||||||
|   Permissions = 6, |   Permissions = 6, | ||||||
|  |   History = 7, | ||||||
| } | } | ||||||
|  |  | ||||||
| enum ContentRenderType { | enum ContentRenderType { | ||||||
| @@ -902,6 +903,17 @@ export class DocumentDetailComponent | |||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get historyEnabled(): boolean { | ||||||
|  |     return ( | ||||||
|  |       this.settings.get(SETTINGS_KEYS.AUDITLOG_ENABLED) && | ||||||
|  |       this.userIsOwner && | ||||||
|  |       this.permissionsService.currentUserCan( | ||||||
|  |         PermissionAction.View, | ||||||
|  |         PermissionType.History | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   notesUpdated(notes: DocumentNote[]) { |   notesUpdated(notes: DocumentNote[]) { | ||||||
|     this.document.notes = notes |     this.document.notes = notes | ||||||
|     this.openDocumentService.refreshDocument(this.documentId) |     this.openDocumentService.refreshDocument(this.documentId) | ||||||
|   | |||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | @if (loading) { | ||||||
|  |     <div class="d-flex"> | ||||||
|  |         <div class="spinner-border spinner-border-sm fw-normal" role="status"></div> | ||||||
|  |     </div> | ||||||
|  | } @else { | ||||||
|  |     <ul class="list-group"> | ||||||
|  |         @if (entries.length === 0) { | ||||||
|  |             <li class="list-group-item"> | ||||||
|  |                 <div class="d-flex justify-content-center"> | ||||||
|  |                     <span class="fst-italic" i18n>No entries found.</span> | ||||||
|  |                 </div> | ||||||
|  |             </li> | ||||||
|  |         } @else { | ||||||
|  |             @for (entry of entries; track entry.id) { | ||||||
|  |                 <li class="list-group-item"> | ||||||
|  |                     <div class="d-flex justify-content-between align-items-center"> | ||||||
|  |                         <ng-template #timestamp> | ||||||
|  |                             <div class="text-light"> | ||||||
|  |                                 {{ entry.timestamp | customDate:'longDate' }} {{ entry.timestamp | date:'shortTime' }} | ||||||
|  |                             </div> | ||||||
|  |                         </ng-template> | ||||||
|  |                         <span class="text-muted" [ngbTooltip]="timestamp">{{ entry.timestamp | customDate:'relative' }}</span> | ||||||
|  |                         @if (entry.actor) { | ||||||
|  |                             <span class="ms-3 fst-italic">{{ entry.actor.username }}</span> | ||||||
|  |                         } @else { | ||||||
|  |                             <span class="ms-3 fst-italic">System</span> | ||||||
|  |                         } | ||||||
|  |                         <span class="badge bg-secondary ms-auto" [class.bg-primary]="entry.action === AuditLogAction.Create">{{ entry.action | titlecase }}</span> | ||||||
|  |                     </div> | ||||||
|  |                     @if (entry.action === AuditLogAction.Update) { | ||||||
|  |                         <ul class="mt-2"> | ||||||
|  |                             @for (change of entry.changes | keyvalue; track change.key) { | ||||||
|  |                                 @if (change.value["type"] === 'm2m') { | ||||||
|  |                                     <li> | ||||||
|  |                                         <span class="fst-italic" i18n>{{ change.value["operation"] | titlecase }}</span>  | ||||||
|  |                                         <span class="text-light">{{ change.key | titlecase }}</span>:  | ||||||
|  |                                         <code class="text-primary">{{ change.value["objects"].join(', ') }}</code> | ||||||
|  |                                     </li> | ||||||
|  |                                 } | ||||||
|  |                                 @else if (change.value["type"] === 'custom_field') { | ||||||
|  |                                     <li> | ||||||
|  |                                         <span class="text-light">{{ change.value["field"] }}</span>:  | ||||||
|  |                                         <code class="text-primary">{{ change.value["value"] }}</code> | ||||||
|  |                                     </li> | ||||||
|  |                                 } | ||||||
|  |                                 @else { | ||||||
|  |                                     <li> | ||||||
|  |                                         <span class="text-light">{{ change.key | titlecase }}</span>:  | ||||||
|  |                                         <code class="text-primary">{{ change.value[1] }}</code> | ||||||
|  |                                     </li> | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         </ul> | ||||||
|  |                     } | ||||||
|  |                 </li> | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     </ul> | ||||||
|  | } | ||||||
| @@ -0,0 +1,57 @@ | |||||||
|  | import { ComponentFixture, TestBed } from '@angular/core/testing' | ||||||
|  |  | ||||||
|  | import { DocumentHistoryComponent } from './document-history.component' | ||||||
|  | import { DocumentService } from 'src/app/services/rest/document.service' | ||||||
|  | import { of } from 'rxjs' | ||||||
|  | import { AuditLogAction } from 'src/app/data/auditlog-entry' | ||||||
|  | import { HttpClientTestingModule } from '@angular/common/http/testing' | ||||||
|  | import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | ||||||
|  | import { DatePipe } from '@angular/common' | ||||||
|  | import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  | import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | ||||||
|  |  | ||||||
|  | describe('DocumentHistoryComponent', () => { | ||||||
|  |   let component: DocumentHistoryComponent | ||||||
|  |   let fixture: ComponentFixture<DocumentHistoryComponent> | ||||||
|  |   let documentService: DocumentService | ||||||
|  |  | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     await TestBed.configureTestingModule({ | ||||||
|  |       declarations: [DocumentHistoryComponent, CustomDatePipe], | ||||||
|  |       providers: [DatePipe], | ||||||
|  |       imports: [ | ||||||
|  |         HttpClientTestingModule, | ||||||
|  |         NgbCollapseModule, | ||||||
|  |         NgxBootstrapIconsModule.pick(allIcons), | ||||||
|  |       ], | ||||||
|  |     }).compileComponents() | ||||||
|  |  | ||||||
|  |     fixture = TestBed.createComponent(DocumentHistoryComponent) | ||||||
|  |     documentService = TestBed.inject(DocumentService) | ||||||
|  |     component = fixture.componentInstance | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should get audit log entries on init', () => { | ||||||
|  |     const getHistorySpy = jest.spyOn(documentService, 'getHistory') | ||||||
|  |     getHistorySpy.mockReturnValue( | ||||||
|  |       of([ | ||||||
|  |         { | ||||||
|  |           id: 1, | ||||||
|  |           actor: { | ||||||
|  |             id: 1, | ||||||
|  |             username: 'user1', | ||||||
|  |           }, | ||||||
|  |           action: AuditLogAction.Create, | ||||||
|  |           timestamp: '2021-01-01T00:00:00Z', | ||||||
|  |           remote_addr: '1.2.3.4', | ||||||
|  |           changes: { | ||||||
|  |             title: ['old title', 'new title'], | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       ]) | ||||||
|  |     ) | ||||||
|  |     component.documentId = 1 | ||||||
|  |     fixture.detectChanges() | ||||||
|  |     expect(getHistorySpy).toHaveBeenCalledWith(1) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | import { Component, Input, OnInit } from '@angular/core' | ||||||
|  | import { AuditLogAction, AuditLogEntry } from 'src/app/data/auditlog-entry' | ||||||
|  | import { DocumentService } from 'src/app/services/rest/document.service' | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   selector: 'pngx-document-history', | ||||||
|  |   templateUrl: './document-history.component.html', | ||||||
|  |   styleUrl: './document-history.component.scss', | ||||||
|  | }) | ||||||
|  | export class DocumentHistoryComponent implements OnInit { | ||||||
|  |   public AuditLogAction = AuditLogAction | ||||||
|  |  | ||||||
|  |   private _documentId: number | ||||||
|  |   @Input() | ||||||
|  |   set documentId(id: number) { | ||||||
|  |     this._documentId = id | ||||||
|  |     this.ngOnInit() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public loading: boolean = true | ||||||
|  |   public entries: AuditLogEntry[] = [] | ||||||
|  |  | ||||||
|  |   constructor(private documentService: DocumentService) {} | ||||||
|  |  | ||||||
|  |   ngOnInit(): void { | ||||||
|  |     if (this._documentId) { | ||||||
|  |       this.loading = true | ||||||
|  |       this.documentService | ||||||
|  |         .getHistory(this._documentId) | ||||||
|  |         .subscribe((auditLogEntries) => { | ||||||
|  |           this.entries = auditLogEntries | ||||||
|  |           this.loading = false | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								src-ui/src/app/data/auditlog-entry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src-ui/src/app/data/auditlog-entry.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | import { User } from './user' | ||||||
|  |  | ||||||
|  | export enum AuditLogAction { | ||||||
|  |   Create = 'create', | ||||||
|  |   Update = 'update', | ||||||
|  |   Delete = 'delete', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface AuditLogEntry { | ||||||
|  |   id: number | ||||||
|  |   timestamp: string | ||||||
|  |   action: AuditLogAction | ||||||
|  |   changes: { | ||||||
|  |     [key: string]: string[] | ||||||
|  |   } | ||||||
|  |   remote_addr: string | ||||||
|  |   actor?: User | ||||||
|  | } | ||||||
| @@ -37,6 +37,7 @@ export const SETTINGS_KEYS = { | |||||||
|   NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: |   NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD: | ||||||
|     'general-settings:notifications:consumer-suppress-on-dashboard', |     'general-settings:notifications:consumer-suppress-on-dashboard', | ||||||
|   NOTES_ENABLED: 'general-settings:notes-enabled', |   NOTES_ENABLED: 'general-settings:notes-enabled', | ||||||
|  |   AUDITLOG_ENABLED: 'general-settings:auditlog-enabled', | ||||||
|   SLIM_SIDEBAR: 'general-settings:slim-sidebar', |   SLIM_SIDEBAR: 'general-settings:slim-sidebar', | ||||||
|   UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled', |   UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled', | ||||||
|   UPDATE_CHECKING_BACKEND_SETTING: |   UPDATE_CHECKING_BACKEND_SETTING: | ||||||
| @@ -143,6 +144,11 @@ export const SETTINGS: UiSetting[] = [ | |||||||
|     type: 'boolean', |     type: 'boolean', | ||||||
|     default: true, |     default: true, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     key: SETTINGS_KEYS.AUDITLOG_ENABLED, | ||||||
|  |     type: 'boolean', | ||||||
|  |     default: true, | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, |     key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, | ||||||
|     type: 'boolean', |     type: 'boolean', | ||||||
|   | |||||||
| @@ -30,4 +30,14 @@ describe('CustomDatePipe', () => { | |||||||
|       ) |       ) | ||||||
|     ).toEqual('2023-05-04') |     ).toEqual('2023-05-04') | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   it('should support relative date formatting', () => { | ||||||
|  |     const now = new Date() | ||||||
|  |     const notNow = new Date(now) | ||||||
|  |     notNow.setDate(now.getDate() - 1) | ||||||
|  |     expect(datePipe.transform(notNow, 'relative')).toEqual('1 day ago') | ||||||
|  |     notNow.setDate(now.getDate() - 2) | ||||||
|  |     expect(datePipe.transform(notNow, 'relative')).toEqual('2 days ago') | ||||||
|  |     expect(datePipe.transform(now, 'relative')).toEqual('Just now') | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -34,6 +34,51 @@ export class CustomDatePipe implements PipeTransform { | |||||||
|       this.settings.get(SETTINGS_KEYS.DATE_LOCALE) || |       this.settings.get(SETTINGS_KEYS.DATE_LOCALE) || | ||||||
|       this.defaultLocale |       this.defaultLocale | ||||||
|     let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT) |     let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT) | ||||||
|  |     if (format === 'relative') { | ||||||
|  |       const seconds = Math.floor((+new Date() - +new Date(value)) / 1000) | ||||||
|  |       if (seconds < 60) return $localize`Just now` | ||||||
|  |       const intervals = { | ||||||
|  |         year: { | ||||||
|  |           label: $localize`year ago`, | ||||||
|  |           labelPlural: $localize`years ago`, | ||||||
|  |           interval: 31536000, | ||||||
|  |         }, | ||||||
|  |         month: { | ||||||
|  |           label: $localize`month ago`, | ||||||
|  |           labelPlural: $localize`months ago`, | ||||||
|  |           interval: 2592000, | ||||||
|  |         }, | ||||||
|  |         week: { | ||||||
|  |           label: $localize`week ago`, | ||||||
|  |           labelPlural: $localize`weeks ago`, | ||||||
|  |           interval: 604800, | ||||||
|  |         }, | ||||||
|  |         day: { | ||||||
|  |           label: $localize`day ago`, | ||||||
|  |           labelPlural: $localize`days ago`, | ||||||
|  |           interval: 86400, | ||||||
|  |         }, | ||||||
|  |         hour: { | ||||||
|  |           label: $localize`hour ago`, | ||||||
|  |           labelPlural: $localize`hours ago`, | ||||||
|  |           interval: 3600, | ||||||
|  |         }, | ||||||
|  |         minute: { | ||||||
|  |           label: $localize`minute ago`, | ||||||
|  |           labelPlural: $localize`minutes ago`, | ||||||
|  |           interval: 60, | ||||||
|  |         }, | ||||||
|  |       } | ||||||
|  |       let counter | ||||||
|  |       for (const i in intervals) { | ||||||
|  |         counter = Math.floor(seconds / intervals[i].interval) | ||||||
|  |         if (counter > 0) { | ||||||
|  |           const label = | ||||||
|  |             counter > 1 ? intervals[i].labelPlural : intervals[i].label | ||||||
|  |           return `${counter} ${label}` | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     if (l == 'iso-8601') { |     if (l == 'iso-8601') { | ||||||
|       return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) |       return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ export enum PermissionType { | |||||||
|   PaperlessTask = '%s_paperlesstask', |   PaperlessTask = '%s_paperlesstask', | ||||||
|   AppConfig = '%s_applicationconfiguration', |   AppConfig = '%s_applicationconfiguration', | ||||||
|   UISettings = '%s_uisettings', |   UISettings = '%s_uisettings', | ||||||
|  |   History = '%s_logentry', | ||||||
|   Note = '%s_note', |   Note = '%s_note', | ||||||
|   MailAccount = '%s_mailaccount', |   MailAccount = '%s_mailaccount', | ||||||
|   MailRule = '%s_mailrule', |   MailRule = '%s_mailrule', | ||||||
|   | |||||||
| @@ -266,6 +266,13 @@ describe(`DocumentService`, () => { | |||||||
|     ) |     ) | ||||||
|     expect(req.request.body.remove_inbox_tags).toEqual(true) |     expect(req.request.body.remove_inbox_tags).toEqual(true) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   it('should call appropriate api endpoint for getting audit log', () => { | ||||||
|  |     subscription = service.getHistory(documents[0].id).subscribe() | ||||||
|  |     const req = httpTestingController.expectOne( | ||||||
|  |       `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/history/` | ||||||
|  |     ) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| afterEach(() => { | afterEach(() => { | ||||||
|   | |||||||
| @@ -19,7 +19,8 @@ import { | |||||||
|   PermissionsService, |   PermissionsService, | ||||||
| } from '../permissions.service' | } from '../permissions.service' | ||||||
| import { SettingsService } from '../settings.service' | import { SettingsService } from '../settings.service' | ||||||
| import { SETTINGS, SETTINGS_KEYS } from 'src/app/data/ui-settings' | import { SETTINGS_KEYS } from 'src/app/data/ui-settings' | ||||||
|  | import { AuditLogEntry } from 'src/app/data/auditlog-entry' | ||||||
|  |  | ||||||
| export const DOCUMENT_SORT_FIELDS = [ | export const DOCUMENT_SORT_FIELDS = [ | ||||||
|   { field: 'archive_serial_number', name: $localize`ASN` }, |   { field: 'archive_serial_number', name: $localize`ASN` }, | ||||||
| @@ -222,6 +223,10 @@ export class DocumentService extends AbstractPaperlessService<Document> { | |||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   getHistory(id: number): Observable<AuditLogEntry[]> { | ||||||
|  |     return this.http.get<AuditLogEntry[]>(this.getResourceUrl(id, 'history')) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   bulkDownload( |   bulkDownload( | ||||||
|     ids: number[], |     ids: number[], | ||||||
|     content = 'both', |     content = 'both', | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ describe('SettingsService', () => { | |||||||
|       update_checking: { enabled: false, backend_setting: 'default' }, |       update_checking: { enabled: false, backend_setting: 'default' }, | ||||||
|       saved_views: { warn_on_unsaved_change: true }, |       saved_views: { warn_on_unsaved_change: true }, | ||||||
|       notes_enabled: true, |       notes_enabled: true, | ||||||
|  |       auditlog_enabled: true, | ||||||
|       tour_complete: false, |       tour_complete: false, | ||||||
|       permissions: { |       permissions: { | ||||||
|         default_owner: null, |         default_owner: null, | ||||||
|   | |||||||
| @@ -882,7 +882,12 @@ class CustomFieldInstance(models.Model): | |||||||
|  |  | ||||||
|  |  | ||||||
| if settings.AUDIT_LOG_ENABLED: | if settings.AUDIT_LOG_ENABLED: | ||||||
|     auditlog.register(Document, m2m_fields={"tags"}) |     auditlog.register( | ||||||
|  |         Document, | ||||||
|  |         m2m_fields={"tags"}, | ||||||
|  |         mask_fields=["content"], | ||||||
|  |         exclude_fields=["modified"], | ||||||
|  |     ) | ||||||
|     auditlog.register(Correspondent) |     auditlog.register(Correspondent) | ||||||
|     auditlog.register(Tag) |     auditlog.register(Tag) | ||||||
|     auditlog.register(DocumentType) |     auditlog.register(DocumentType) | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import zoneinfo | |||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
|  |  | ||||||
| import magic | import magic | ||||||
|  | from auditlog.context import set_actor | ||||||
| from celery import states | 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 | ||||||
| @@ -746,7 +747,11 @@ class DocumentSerializer( | |||||||
|                     for tag in instance.tags.all() |                     for tag in instance.tags.all() | ||||||
|                     if tag not in inbox_tags_not_being_added |                     if tag not in inbox_tags_not_being_added | ||||||
|                 ] |                 ] | ||||||
|         super().update(instance, validated_data) |         if settings.AUDIT_LOG_ENABLED: | ||||||
|  |             with set_actor(self.user): | ||||||
|  |                 super().update(instance, validated_data) | ||||||
|  |         else: | ||||||
|  |             super().update(instance, validated_data) | ||||||
|         return instance |         return instance | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|   | |||||||
| @@ -316,6 +316,133 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): | |||||||
|         response = self.client.get(f"/api/documents/{doc.pk}/thumb/") |         response = self.client.get(f"/api/documents/{doc.pk}/thumb/") | ||||||
|         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) |         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) | ||||||
|  |  | ||||||
|  |     def test_document_history_action(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Document | ||||||
|  |         WHEN: | ||||||
|  |             - Document is updated | ||||||
|  |         THEN: | ||||||
|  |             - Audit log contains changes | ||||||
|  |         """ | ||||||
|  |         doc = Document.objects.create( | ||||||
|  |             title="First title", | ||||||
|  |             checksum="123", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |         ) | ||||||
|  |         self.client.force_login(user=self.user) | ||||||
|  |         self.client.patch( | ||||||
|  |             f"/api/documents/{doc.pk}/", | ||||||
|  |             {"title": "New title"}, | ||||||
|  |             format="json", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get(f"/api/documents/{doc.pk}/history/") | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||||
|  |         self.assertEqual(len(response.data), 2) | ||||||
|  |         self.assertEqual(response.data[0]["actor"]["id"], self.user.id) | ||||||
|  |         self.assertEqual(response.data[0]["action"], "update") | ||||||
|  |         self.assertEqual( | ||||||
|  |             response.data[0]["changes"], | ||||||
|  |             {"title": ["First title", "New title"]}, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_document_history_action_w_custom_fields(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Document with custom fields | ||||||
|  |         WHEN: | ||||||
|  |             - Document is updated | ||||||
|  |         THEN: | ||||||
|  |             - Audit log contains custom field changes | ||||||
|  |         """ | ||||||
|  |         doc = Document.objects.create( | ||||||
|  |             title="First title", | ||||||
|  |             checksum="123", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |         ) | ||||||
|  |         custom_field = CustomField.objects.create( | ||||||
|  |             name="custom field str", | ||||||
|  |             data_type=CustomField.FieldDataType.STRING, | ||||||
|  |         ) | ||||||
|  |         self.client.force_login(user=self.user) | ||||||
|  |         self.client.patch( | ||||||
|  |             f"/api/documents/{doc.pk}/", | ||||||
|  |             data={ | ||||||
|  |                 "custom_fields": [ | ||||||
|  |                     { | ||||||
|  |                         "field": custom_field.pk, | ||||||
|  |                         "value": "custom value", | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |             format="json", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get(f"/api/documents/{doc.pk}/history/") | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||||
|  |         self.assertEqual(response.data[1]["actor"]["id"], self.user.id) | ||||||
|  |         self.assertEqual(response.data[1]["action"], "create") | ||||||
|  |         self.assertEqual( | ||||||
|  |             response.data[1]["changes"], | ||||||
|  |             { | ||||||
|  |                 "custom_fields": { | ||||||
|  |                     "type": "custom_field", | ||||||
|  |                     "field": "custom field str", | ||||||
|  |                     "value": "custom value", | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @override_settings(AUDIT_LOG_ENABLED=False) | ||||||
|  |     def test_document_history_action_disabled(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Audit log is disabled | ||||||
|  |         WHEN: | ||||||
|  |             - Document is updated | ||||||
|  |             - Audit log is requested | ||||||
|  |         THEN: | ||||||
|  |             - Audit log returns HTTP 400 Bad Request | ||||||
|  |         """ | ||||||
|  |         doc = Document.objects.create( | ||||||
|  |             title="First title", | ||||||
|  |             checksum="123", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |         ) | ||||||
|  |         self.client.force_login(user=self.user) | ||||||
|  |         self.client.patch( | ||||||
|  |             f"/api/documents/{doc.pk}/", | ||||||
|  |             {"title": "New title"}, | ||||||
|  |             format="json", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get(f"/api/documents/{doc.pk}/history/") | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||||||
|  |  | ||||||
|  |     def test_document_history_insufficient_perms(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Audit log is disabled | ||||||
|  |         WHEN: | ||||||
|  |             - Document is updated | ||||||
|  |             - Audit log is requested | ||||||
|  |         THEN: | ||||||
|  |             - Audit log returns HTTP 400 Bad Request | ||||||
|  |         """ | ||||||
|  |         user = User.objects.create_user(username="test") | ||||||
|  |         user.user_permissions.add(*Permission.objects.filter(codename="view_document")) | ||||||
|  |         self.client.force_login(user=user) | ||||||
|  |         doc = Document.objects.create( | ||||||
|  |             title="First title", | ||||||
|  |             checksum="123", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |             owner=user, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get(f"/api/documents/{doc.pk}/history/") | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) | ||||||
|  |  | ||||||
|     def test_document_filters(self): |     def test_document_filters(self): | ||||||
|         doc1 = Document.objects.create( |         doc1 = Document.objects.create( | ||||||
|             title="none1", |             title="none1", | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase): | |||||||
|             { |             { | ||||||
|                 "app_title": None, |                 "app_title": None, | ||||||
|                 "app_logo": None, |                 "app_logo": None, | ||||||
|  |                 "auditlog_enabled": True, | ||||||
|                 "update_checking": { |                 "update_checking": { | ||||||
|                     "backend_setting": "default", |                     "backend_setting": "default", | ||||||
|                 }, |                 }, | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import tempfile | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from unittest import mock | from unittest import mock | ||||||
|  |  | ||||||
|  | from auditlog.context import disable_auditlog | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.db import DatabaseError | from django.db import DatabaseError | ||||||
| @@ -143,7 +144,9 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | |||||||
|         # Set a correspondent and save the document |         # Set a correspondent and save the document | ||||||
|         document.correspondent = Correspondent.objects.get_or_create(name="test")[0] |         document.correspondent = Correspondent.objects.get_or_create(name="test")[0] | ||||||
|  |  | ||||||
|         with mock.patch("documents.signals.handlers.Document.objects.filter") as m: |         with mock.patch( | ||||||
|  |             "documents.signals.handlers.Document.objects.filter", | ||||||
|  |         ) as m, disable_auditlog(): | ||||||
|             m.side_effect = DatabaseError() |             m.side_effect = DatabaseError() | ||||||
|             document.save() |             document.save() | ||||||
|  |  | ||||||
| @@ -557,20 +560,21 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | |||||||
|     @override_settings(FILENAME_FORMAT="{title}") |     @override_settings(FILENAME_FORMAT="{title}") | ||||||
|     @mock.patch("documents.signals.handlers.Document.objects.filter") |     @mock.patch("documents.signals.handlers.Document.objects.filter") | ||||||
|     def test_no_update_without_change(self, m): |     def test_no_update_without_change(self, m): | ||||||
|         doc = Document.objects.create( |         with disable_auditlog(): | ||||||
|             title="document", |             doc = Document.objects.create( | ||||||
|             filename="document.pdf", |                 title="document", | ||||||
|             archive_filename="document.pdf", |                 filename="document.pdf", | ||||||
|             checksum="A", |                 archive_filename="document.pdf", | ||||||
|             archive_checksum="B", |                 checksum="A", | ||||||
|             mime_type="application/pdf", |                 archive_checksum="B", | ||||||
|         ) |                 mime_type="application/pdf", | ||||||
|         Path(doc.source_path).touch() |             ) | ||||||
|         Path(doc.archive_path).touch() |             Path(doc.source_path).touch() | ||||||
|  |             Path(doc.archive_path).touch() | ||||||
|  |  | ||||||
|         doc.save() |             doc.save() | ||||||
|  |  | ||||||
|         m.assert_not_called() |             m.assert_not_called() | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ import pathvalidate | |||||||
| from django.apps import apps | from django.apps import apps | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
|  | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.db import connections | from django.db import connections | ||||||
| from django.db.migrations.loader import MigrationLoader | from django.db.migrations.loader import MigrationLoader | ||||||
| from django.db.migrations.recorder import MigrationRecorder | from django.db.migrations.recorder import MigrationRecorder | ||||||
| @@ -105,6 +106,7 @@ from documents.matching import match_storage_paths | |||||||
| from documents.matching import match_tags | from documents.matching import match_tags | ||||||
| from documents.models import Correspondent | from documents.models import Correspondent | ||||||
| from documents.models import CustomField | from documents.models import CustomField | ||||||
|  | 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 Note | from documents.models import Note | ||||||
| @@ -729,6 +731,66 @@ class DocumentViewSet( | |||||||
|             ] |             ] | ||||||
|             return Response(links) |             return Response(links) | ||||||
|  |  | ||||||
|  |     @action(methods=["get"], detail=True, name="Audit Trail") | ||||||
|  |     def history(self, request, pk=None): | ||||||
|  |         if not settings.AUDIT_LOG_ENABLED: | ||||||
|  |             return HttpResponseBadRequest("Audit log is disabled") | ||||||
|  |         try: | ||||||
|  |             doc = Document.objects.get(pk=pk) | ||||||
|  |             if not request.user.has_perm("auditlog.view_logentry") or ( | ||||||
|  |                 doc.owner is not None and doc.owner != request.user | ||||||
|  |             ): | ||||||
|  |                 return HttpResponseForbidden( | ||||||
|  |                     "Insufficient permissions", | ||||||
|  |                 ) | ||||||
|  |         except Document.DoesNotExist:  # pragma: no cover | ||||||
|  |             raise Http404 | ||||||
|  |  | ||||||
|  |         # documents | ||||||
|  |         entries = [ | ||||||
|  |             { | ||||||
|  |                 "id": entry.id, | ||||||
|  |                 "timestamp": entry.timestamp, | ||||||
|  |                 "action": entry.get_action_display(), | ||||||
|  |                 "changes": entry.changes, | ||||||
|  |                 "actor": ( | ||||||
|  |                     {"id": entry.actor.id, "username": entry.actor.username} | ||||||
|  |                     if entry.actor | ||||||
|  |                     else None | ||||||
|  |                 ), | ||||||
|  |             } | ||||||
|  |             for entry in LogEntry.objects.filter(object_pk=doc.pk).select_related( | ||||||
|  |                 "actor", | ||||||
|  |             ) | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         # custom fields | ||||||
|  |         for entry in LogEntry.objects.filter( | ||||||
|  |             object_pk__in=doc.custom_fields.values_list("id", flat=True), | ||||||
|  |             content_type=ContentType.objects.get_for_model(CustomFieldInstance), | ||||||
|  |         ).select_related("actor"): | ||||||
|  |             entries.append( | ||||||
|  |                 { | ||||||
|  |                     "id": entry.id, | ||||||
|  |                     "timestamp": entry.timestamp, | ||||||
|  |                     "action": entry.get_action_display(), | ||||||
|  |                     "changes": { | ||||||
|  |                         "custom_fields": { | ||||||
|  |                             "type": "custom_field", | ||||||
|  |                             "field": str(entry.object_repr).split(":")[0].strip(), | ||||||
|  |                             "value": str(entry.object_repr).split(":")[1].strip(), | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                     "actor": ( | ||||||
|  |                         {"id": entry.actor.id, "username": entry.actor.username} | ||||||
|  |                         if entry.actor | ||||||
|  |                         else None | ||||||
|  |                     ), | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         return Response(sorted(entries, key=lambda x: x["timestamp"], reverse=True)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SearchResultSerializer(DocumentSerializer, PassUserMixin): | class SearchResultSerializer(DocumentSerializer, PassUserMixin): | ||||||
|     def to_representation(self, instance): |     def to_representation(self, instance): | ||||||
| @@ -1267,6 +1329,8 @@ class UiSettingsView(GenericAPIView): | |||||||
|         if general_config.app_logo is not None and len(general_config.app_logo) > 0: |         if general_config.app_logo is not None and len(general_config.app_logo) > 0: | ||||||
|             ui_settings["app_logo"] = general_config.app_logo |             ui_settings["app_logo"] = general_config.app_logo | ||||||
|  |  | ||||||
|  |         ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED | ||||||
|  |  | ||||||
|         user_resp = { |         user_resp = { | ||||||
|             "id": user.id, |             "id": user.id, | ||||||
|             "username": user.username, |             "username": user.username, | ||||||
|   | |||||||
| @@ -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: 2024-04-19 01:13-0700\n" | "POT-Creation-Date: 2024-04-19 01:15-0700\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" | ||||||
| @@ -25,27 +25,27 @@ msgstr "" | |||||||
| msgid "owner" | msgid "owner" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:53 documents/models.py:897 | #: documents/models.py:53 documents/models.py:902 | ||||||
| msgid "None" | msgid "None" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:54 documents/models.py:898 | #: documents/models.py:54 documents/models.py:903 | ||||||
| msgid "Any word" | msgid "Any word" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:55 documents/models.py:899 | #: documents/models.py:55 documents/models.py:904 | ||||||
| msgid "All words" | msgid "All words" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:56 documents/models.py:900 | #: documents/models.py:56 documents/models.py:905 | ||||||
| msgid "Exact match" | msgid "Exact match" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:57 documents/models.py:901 | #: documents/models.py:57 documents/models.py:906 | ||||||
| msgid "Regular expression" | msgid "Regular expression" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:58 documents/models.py:902 | #: documents/models.py:58 documents/models.py:907 | ||||||
| msgid "Fuzzy word" | msgid "Fuzzy word" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @@ -53,20 +53,20 @@ msgstr "" | |||||||
| msgid "Automatic" | msgid "Automatic" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:62 documents/models.py:397 documents/models.py:1218 | #: documents/models.py:62 documents/models.py:397 documents/models.py:1223 | ||||||
| #: paperless_mail/models.py:18 paperless_mail/models.py:93 | #: paperless_mail/models.py:18 paperless_mail/models.py:93 | ||||||
| msgid "name" | msgid "name" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:64 documents/models.py:958 | #: documents/models.py:64 documents/models.py:963 | ||||||
| msgid "match" | msgid "match" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:67 documents/models.py:961 | #: documents/models.py:67 documents/models.py:966 | ||||||
| msgid "matching algorithm" | msgid "matching algorithm" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:72 documents/models.py:966 | #: documents/models.py:72 documents/models.py:971 | ||||||
| msgid "is insensitive" | msgid "is insensitive" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @@ -615,246 +615,246 @@ msgstr "" | |||||||
| msgid "custom field instances" | msgid "custom field instances" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:905 | #: documents/models.py:910 | ||||||
| msgid "Consumption Started" | msgid "Consumption Started" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:906 | #: documents/models.py:911 | ||||||
| msgid "Document Added" | msgid "Document Added" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:907 | #: documents/models.py:912 | ||||||
| msgid "Document Updated" | msgid "Document Updated" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:910 | #: documents/models.py:915 | ||||||
| msgid "Consume Folder" | msgid "Consume Folder" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:911 | #: documents/models.py:916 | ||||||
| msgid "Api Upload" | msgid "Api Upload" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:912 | #: documents/models.py:917 | ||||||
| msgid "Mail Fetch" | msgid "Mail Fetch" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:915 | #: documents/models.py:920 | ||||||
| msgid "Workflow Trigger Type" | msgid "Workflow Trigger Type" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:927 | #: documents/models.py:932 | ||||||
| msgid "filter path" | msgid "filter path" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:932 | #: documents/models.py:937 | ||||||
| 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:939 | #: documents/models.py:944 | ||||||
| msgid "filter filename" | msgid "filter filename" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:944 paperless_mail/models.py:148 | #: documents/models.py:949 paperless_mail/models.py:148 | ||||||
| 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:955 | #: documents/models.py:960 | ||||||
| msgid "filter documents from this mail rule" | msgid "filter documents from this mail rule" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:971 | #: documents/models.py:976 | ||||||
| msgid "has these tag(s)" | msgid "has these tag(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:979 | #: documents/models.py:984 | ||||||
| msgid "has this document type" | msgid "has this document type" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:987 | #: documents/models.py:992 | ||||||
| msgid "has this correspondent" | msgid "has this correspondent" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:991 | #: documents/models.py:996 | ||||||
| msgid "workflow trigger" | msgid "workflow trigger" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:992 | #: documents/models.py:997 | ||||||
| msgid "workflow triggers" | msgid "workflow triggers" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1002 | #: documents/models.py:1007 | ||||||
| msgid "Assignment" | msgid "Assignment" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1006 | #: documents/models.py:1011 | ||||||
| msgid "Removal" | msgid "Removal" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1010 | #: documents/models.py:1015 | ||||||
| msgid "Workflow Action Type" | msgid "Workflow Action Type" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1016 | #: documents/models.py:1021 | ||||||
| msgid "assign title" | msgid "assign title" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1021 | #: documents/models.py:1026 | ||||||
| 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:1030 paperless_mail/models.py:216 | #: documents/models.py:1035 paperless_mail/models.py:216 | ||||||
| msgid "assign this tag" | msgid "assign this tag" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1039 paperless_mail/models.py:224 | #: documents/models.py:1044 paperless_mail/models.py:224 | ||||||
| msgid "assign this document type" | msgid "assign this document type" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1048 paperless_mail/models.py:238 | #: documents/models.py:1053 paperless_mail/models.py:238 | ||||||
| msgid "assign this correspondent" | msgid "assign this correspondent" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1057 | #: documents/models.py:1062 | ||||||
| msgid "assign this storage path" | msgid "assign this storage path" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1066 | #: documents/models.py:1071 | ||||||
| msgid "assign this owner" | msgid "assign this owner" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1073 | #: documents/models.py:1078 | ||||||
| msgid "grant view permissions to these users" | msgid "grant view permissions to these users" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1080 | #: documents/models.py:1085 | ||||||
| msgid "grant view permissions to these groups" | msgid "grant view permissions to these groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1087 | #: documents/models.py:1092 | ||||||
| msgid "grant change permissions to these users" | msgid "grant change permissions to these users" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1094 | #: documents/models.py:1099 | ||||||
| msgid "grant change permissions to these groups" | msgid "grant change permissions to these groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1101 | #: documents/models.py:1106 | ||||||
| msgid "assign these custom fields" | msgid "assign these custom fields" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1108 | #: documents/models.py:1113 | ||||||
| msgid "remove these tag(s)" | msgid "remove these tag(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1113 | #: documents/models.py:1118 | ||||||
| msgid "remove all tags" | msgid "remove all tags" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1120 | #: documents/models.py:1125 | ||||||
| msgid "remove these document type(s)" | msgid "remove these document type(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1125 | #: documents/models.py:1130 | ||||||
| msgid "remove all document types" | msgid "remove all document types" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1132 | #: documents/models.py:1137 | ||||||
| msgid "remove these correspondent(s)" | msgid "remove these correspondent(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1137 | #: documents/models.py:1142 | ||||||
| msgid "remove all correspondents" | msgid "remove all correspondents" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1144 | #: documents/models.py:1149 | ||||||
| msgid "remove these storage path(s)" | msgid "remove these storage path(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1149 | #: documents/models.py:1154 | ||||||
| msgid "remove all storage paths" | msgid "remove all storage paths" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1156 | #: documents/models.py:1161 | ||||||
| msgid "remove these owner(s)" | msgid "remove these owner(s)" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1161 | #: documents/models.py:1166 | ||||||
| msgid "remove all owners" | msgid "remove all owners" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1168 | #: documents/models.py:1173 | ||||||
| msgid "remove view permissions for these users" | msgid "remove view permissions for these users" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1175 | #: documents/models.py:1180 | ||||||
| msgid "remove view permissions for these groups" | msgid "remove view permissions for these groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1182 | #: documents/models.py:1187 | ||||||
| msgid "remove change permissions for these users" | msgid "remove change permissions for these users" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1189 | #: documents/models.py:1194 | ||||||
| msgid "remove change permissions for these groups" | msgid "remove change permissions for these groups" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1194 | #: documents/models.py:1199 | ||||||
| msgid "remove all permissions" | msgid "remove all permissions" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1201 | #: documents/models.py:1206 | ||||||
| msgid "remove these custom fields" | msgid "remove these custom fields" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1206 | #: documents/models.py:1211 | ||||||
| msgid "remove all custom fields" | msgid "remove all custom fields" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1210 | #: documents/models.py:1215 | ||||||
| msgid "workflow action" | msgid "workflow action" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1211 | #: documents/models.py:1216 | ||||||
| msgid "workflow actions" | msgid "workflow actions" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1220 paperless_mail/models.py:95 | #: documents/models.py:1225 paperless_mail/models.py:95 | ||||||
| msgid "order" | msgid "order" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1226 | #: documents/models.py:1231 | ||||||
| msgid "triggers" | msgid "triggers" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1233 | #: documents/models.py:1238 | ||||||
| msgid "actions" | msgid "actions" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/models.py:1236 | #: documents/models.py:1241 | ||||||
| msgid "enabled" | msgid "enabled" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:114 | #: documents/serialisers.py:115 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Invalid regular expression: %(error)s" | msgid "Invalid regular expression: %(error)s" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:417 | #: documents/serialisers.py:418 | ||||||
| msgid "Invalid color." | msgid "Invalid color." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1143 | #: documents/serialisers.py:1148 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "File type %(type)s not supported" | msgid "File type %(type)s not supported" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1252 | #: documents/serialisers.py:1257 | ||||||
| msgid "Invalid variable detected." | msgid "Invalid variable detected." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon