mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Feature: document history (audit log UI) (#6388)
This commit is contained in:
parent
d65fcf70f3
commit
05b1ff9738
8
Pipfile.lock
generated
8
Pipfile.lock
generated
@ -479,12 +479,12 @@
|
||||
},
|
||||
"django-auditlog": {
|
||||
"hashes": [
|
||||
"sha256:7bc2c87e4aff62dec9785d1b2359a2b27148f8c286f8a52b9114fc7876c5a9f7",
|
||||
"sha256:b9d3acebb64f3f2785157efe3f2f802e0929aafc579d85bbfb9827db4adab532"
|
||||
"sha256:92db1cf4a51ceca5c26b3ff46997d9e3305a02da1bd435e2efb5b8b6d300ce1f",
|
||||
"sha256:9de49f80a4911135d136017123cd73461f869b4947eec14d5e76db4b88182f3f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.3.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"django-celery-results": {
|
||||
"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>/share_links/`: Retrieve share links for a document.
|
||||
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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}
|
||||
|
||||
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="linenumber">22</context>
|
||||
</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 id="7991430199894172363" datatype="html">
|
||||
<source>Read the documentation about this setting</source>
|
||||
@ -447,7 +451,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="3768927257183755959" datatype="html">
|
||||
@ -506,7 +510,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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 purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||
@ -947,7 +951,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="293524471897878391" datatype="html">
|
||||
@ -977,7 +981,7 @@
|
||||
</context-group>
|
||||
<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 context-type="linenumber">301</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||
@ -1682,7 +1686,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5968132631442328843" datatype="html">
|
||||
@ -2032,7 +2036,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||
@ -2075,15 +2079,15 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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 purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6418218602775540217" datatype="html">
|
||||
@ -4822,7 +4826,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2691296884221415710" datatype="html">
|
||||
@ -4849,7 +4853,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8911158217491828773" datatype="html">
|
||||
@ -5119,7 +5123,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
|
||||
@ -5174,7 +5178,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2091353339965748767" datatype="html">
|
||||
@ -5312,103 +5316,110 @@
|
||||
<context context-type="linenumber">279,282</context>
|
||||
</context-group>
|
||||
</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">
|
||||
<source>Save & next</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4910102545766233758" datatype="html">
|
||||
<source>Save & close</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8191371354890763172" datatype="html">
|
||||
<source>Enter Password</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2218903673684131427" datatype="html">
|
||||
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="3200733026060976258" datatype="html">
|
||||
<source>Document changes detected</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2887155916749964" datatype="html">
|
||||
<source>The version of this document in your browser session appears older than the existing version.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8720977247725652816" datatype="html">
|
||||
<source>Ok</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="5758784066858623886" datatype="html">
|
||||
<source>Error retrieving metadata</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="3456881259945295697" datatype="html">
|
||||
<source>Error retrieving suggestions.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="8348337312757497317" datatype="html">
|
||||
<source>Document saved successfully.</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="448882439049417053" datatype="html">
|
||||
<source>Error saving document</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="9021887951960049161" datatype="html">
|
||||
<source>Confirm delete</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6691075929777935948" datatype="html">
|
||||
<source>The files for this document will be deleted permanently. This operation cannot be undone.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="719892092227206532" datatype="html">
|
||||
<source>Delete document</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7295637485862454066" datatype="html">
|
||||
<source>Error deleting document</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="7362691899087997122" datatype="html">
|
||||
<source>Redo OCR confirm</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4409560272830824468" datatype="html">
|
||||
<source>Error executing operation</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4458954481601077369" datatype="html">
|
||||
<source>Page Fit</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="1217563727923422413" datatype="html">
|
||||
<source>Split confirm</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2805304563009985503" datatype="html">
|
||||
<source>This operation will split the selected document(s) into new documents.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4158171846914923744" datatype="html">
|
||||
<source>Split operation will begin in the background.</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="3235014591864339926" datatype="html">
|
||||
<source>Error executing split operation</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6555329262222566158" datatype="html">
|
||||
<source>Rotate confirm</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4233432423256408453" datatype="html">
|
||||
<source>This will alter the original copy.</source>
|
||||
<context-group purpose="location">
|
||||
<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 purpose="location">
|
||||
<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>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2962674215361798818" datatype="html">
|
||||
<source>Error executing rotate operation</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6857598786757174736" datatype="html">
|
||||
@ -6094,7 +6112,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="6954625430271090777" datatype="html">
|
||||
@ -6126,7 +6144,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="3557446856808034218" datatype="html">
|
||||
@ -6176,7 +6194,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="2179847500064178686" datatype="html">
|
||||
@ -7370,6 +7388,97 @@
|
||||
<context context-type="linenumber">36</context>
|
||||
</context-group>
|
||||
</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">
|
||||
<source>(no title)</source>
|
||||
<context-group purpose="location">
|
||||
@ -7532,14 +7641,14 @@
|
||||
<source>Modified</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
</trans-unit>
|
||||
<trans-unit id="4460262093225954455" datatype="html">
|
||||
<source>Search score</source>
|
||||
<context-group purpose="location">
|
||||
<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>
|
||||
<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>
|
||||
|
@ -119,6 +119,7 @@ import { NgxFilesizeModule } from 'ngx-filesize'
|
||||
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 { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
|
||||
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
|
||||
import {
|
||||
airplane,
|
||||
archive,
|
||||
@ -472,6 +473,7 @@ function initializeApp(settings: SettingsService) {
|
||||
RotateConfirmDialogComponent,
|
||||
MergeConfirmDialogComponent,
|
||||
SplitConfirmDialogComponent,
|
||||
DocumentHistoryComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -9,17 +9,17 @@
|
||||
<div class="col" i18n>Delete</div>
|
||||
<div class="col" i18n>View</div>
|
||||
</li>
|
||||
@for (type of PermissionType | keyvalue; track type) {
|
||||
<li class="list-group-item d-flex" [formGroupName]="type.key">
|
||||
<div class="col-3">{{type.key}}:</div>
|
||||
<div class="col form-check form-check-inline form-switch" [ngbPopover]="inheritedWarning" [disablePopover]="!isInherited(type.key)" 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">
|
||||
<label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
|
||||
@for (type of allowedTypes; track type) {
|
||||
<li class="list-group-item d-flex" [formGroupName]="type">
|
||||
<div class="col-3">{{type}}:</div>
|
||||
<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}}_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}}_all" i18n>All</label>
|
||||
</div>
|
||||
@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">
|
||||
<input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}">
|
||||
<label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
|
||||
<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}}_{{action.key}}" formControlName="{{action.key}}">
|
||||
<label class="form-check-label visually-hidden" for="{{type}}_{{action.key}}" i18n>{{action.key}}</label>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
|
@ -12,6 +12,9 @@ import {
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { By } from '@angular/platform-browser'
|
||||
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 = [
|
||||
'add_document',
|
||||
@ -28,6 +31,7 @@ describe('PermissionsSelectComponent', () => {
|
||||
let component: PermissionsSelectComponent
|
||||
let fixture: ComponentFixture<PermissionsSelectComponent>
|
||||
let permissionsChangeResult: Permissions
|
||||
let settingsService: SettingsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -38,9 +42,11 @@ describe('PermissionsSelectComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
HttpClientTestingModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(PermissionsSelectComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
@ -99,4 +105,11 @@ describe('PermissionsSelectComponent', () => {
|
||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||
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,
|
||||
} from 'src/app/services/permissions.service'
|
||||
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({
|
||||
providers: [
|
||||
@ -60,15 +62,23 @@ export class PermissionsSelectComponent
|
||||
|
||||
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()
|
||||
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({})
|
||||
for (const action in PermissionAction) {
|
||||
control.addControl(action, new FormControl(null))
|
||||
}
|
||||
this.form.addControl(type, control)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
writeValue(permissions: string[]): void {
|
||||
@ -92,7 +102,7 @@ export class PermissionsSelectComponent
|
||||
}
|
||||
}
|
||||
})
|
||||
Object.keys(PermissionType).forEach((type) => {
|
||||
this.allowedTypes.forEach((type) => {
|
||||
if (
|
||||
Object.values(this.form.get(type).value).every((val) => val == true)
|
||||
) {
|
||||
@ -191,7 +201,7 @@ export class PermissionsSelectComponent
|
||||
}
|
||||
|
||||
updateDisabledStates() {
|
||||
for (const type in PermissionType) {
|
||||
this.allowedTypes.forEach((type) => {
|
||||
const control = this.form.get(type)
|
||||
let actionControl: AbstractControl
|
||||
for (const action in PermissionAction) {
|
||||
@ -200,6 +210,6 @@ export class PermissionsSelectComponent
|
||||
? actionControl.disable()
|
||||
: actionControl.enable()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -285,6 +285,17 @@
|
||||
</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) {
|
||||
<li [ngbNavItem]="DocumentDetailNavIDs.Permissions">
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
|
@ -77,6 +77,7 @@ enum DocumentDetailNavIDs {
|
||||
Preview = 4,
|
||||
Notes = 5,
|
||||
Permissions = 6,
|
||||
History = 7,
|
||||
}
|
||||
|
||||
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[]) {
|
||||
this.document.notes = notes
|
||||
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:
|
||||
'general-settings:notifications:consumer-suppress-on-dashboard',
|
||||
NOTES_ENABLED: 'general-settings:notes-enabled',
|
||||
AUDITLOG_ENABLED: 'general-settings:auditlog-enabled',
|
||||
SLIM_SIDEBAR: 'general-settings:slim-sidebar',
|
||||
UPDATE_CHECKING_ENABLED: 'general-settings:update-checking:enabled',
|
||||
UPDATE_CHECKING_BACKEND_SETTING:
|
||||
@ -143,6 +144,11 @@ export const SETTINGS: UiSetting[] = [
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.AUDITLOG_ENABLED,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.UPDATE_CHECKING_ENABLED,
|
||||
type: 'boolean',
|
||||
|
@ -30,4 +30,14 @@ describe('CustomDatePipe', () => {
|
||||
)
|
||||
).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.defaultLocale
|
||||
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') {
|
||||
return this.datePipe.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone)
|
||||
} else {
|
||||
|
@ -19,6 +19,7 @@ export enum PermissionType {
|
||||
PaperlessTask = '%s_paperlesstask',
|
||||
AppConfig = '%s_applicationconfiguration',
|
||||
UISettings = '%s_uisettings',
|
||||
History = '%s_logentry',
|
||||
Note = '%s_note',
|
||||
MailAccount = '%s_mailaccount',
|
||||
MailRule = '%s_mailrule',
|
||||
|
@ -266,6 +266,13 @@ describe(`DocumentService`, () => {
|
||||
)
|
||||
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(() => {
|
||||
|
@ -19,7 +19,8 @@ import {
|
||||
PermissionsService,
|
||||
} from '../permissions.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 = [
|
||||
{ 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(
|
||||
ids: number[],
|
||||
content = 'both',
|
||||
|
@ -47,6 +47,7 @@ describe('SettingsService', () => {
|
||||
update_checking: { enabled: false, backend_setting: 'default' },
|
||||
saved_views: { warn_on_unsaved_change: true },
|
||||
notes_enabled: true,
|
||||
auditlog_enabled: true,
|
||||
tour_complete: false,
|
||||
permissions: {
|
||||
default_owner: null,
|
||||
|
@ -882,7 +882,12 @@ class CustomFieldInstance(models.Model):
|
||||
|
||||
|
||||
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(Tag)
|
||||
auditlog.register(DocumentType)
|
||||
|
@ -5,6 +5,7 @@ import zoneinfo
|
||||
from decimal import Decimal
|
||||
|
||||
import magic
|
||||
from auditlog.context import set_actor
|
||||
from celery import states
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
@ -746,7 +747,11 @@ class DocumentSerializer(
|
||||
for tag in instance.tags.all()
|
||||
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
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -316,6 +316,133 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
response = self.client.get(f"/api/documents/{doc.pk}/thumb/")
|
||||
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):
|
||||
doc1 = Document.objects.create(
|
||||
title="none1",
|
||||
|
@ -39,6 +39,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||
{
|
||||
"app_title": None,
|
||||
"app_logo": None,
|
||||
"auditlog_enabled": True,
|
||||
"update_checking": {
|
||||
"backend_setting": "default",
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from auditlog.context import disable_auditlog
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import DatabaseError
|
||||
@ -143,7 +144,9 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
# Set a correspondent and save the document
|
||||
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()
|
||||
document.save()
|
||||
|
||||
@ -557,20 +560,21 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||
@override_settings(FILENAME_FORMAT="{title}")
|
||||
@mock.patch("documents.signals.handlers.Document.objects.filter")
|
||||
def test_no_update_without_change(self, m):
|
||||
doc = Document.objects.create(
|
||||
title="document",
|
||||
filename="document.pdf",
|
||||
archive_filename="document.pdf",
|
||||
checksum="A",
|
||||
archive_checksum="B",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
Path(doc.source_path).touch()
|
||||
Path(doc.archive_path).touch()
|
||||
with disable_auditlog():
|
||||
doc = Document.objects.create(
|
||||
title="document",
|
||||
filename="document.pdf",
|
||||
archive_filename="document.pdf",
|
||||
checksum="A",
|
||||
archive_checksum="B",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
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):
|
||||
|
@ -18,6 +18,7 @@ import pathvalidate
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import connections
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
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.models import Correspondent
|
||||
from documents.models import CustomField
|
||||
from documents.models import CustomFieldInstance
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import Note
|
||||
@ -729,6 +731,66 @@ class DocumentViewSet(
|
||||
]
|
||||
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):
|
||||
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:
|
||||
ui_settings["app_logo"] = general_config.app_logo
|
||||
|
||||
ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED
|
||||
|
||||
user_resp = {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
|
@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\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"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@ -25,27 +25,27 @@ msgstr ""
|
||||
msgid "owner"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:53 documents/models.py:897
|
||||
#: documents/models.py:53 documents/models.py:902
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:54 documents/models.py:898
|
||||
#: documents/models.py:54 documents/models.py:903
|
||||
msgid "Any word"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:55 documents/models.py:899
|
||||
#: documents/models.py:55 documents/models.py:904
|
||||
msgid "All words"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:56 documents/models.py:900
|
||||
#: documents/models.py:56 documents/models.py:905
|
||||
msgid "Exact match"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:57 documents/models.py:901
|
||||
#: documents/models.py:57 documents/models.py:906
|
||||
msgid "Regular expression"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:58 documents/models.py:902
|
||||
#: documents/models.py:58 documents/models.py:907
|
||||
msgid "Fuzzy word"
|
||||
msgstr ""
|
||||
|
||||
@ -53,20 +53,20 @@ msgstr ""
|
||||
msgid "Automatic"
|
||||
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
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:64 documents/models.py:958
|
||||
#: documents/models.py:64 documents/models.py:963
|
||||
msgid "match"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:67 documents/models.py:961
|
||||
#: documents/models.py:67 documents/models.py:966
|
||||
msgid "matching algorithm"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:72 documents/models.py:966
|
||||
#: documents/models.py:72 documents/models.py:971
|
||||
msgid "is insensitive"
|
||||
msgstr ""
|
||||
|
||||
@ -615,246 +615,246 @@ msgstr ""
|
||||
msgid "custom field instances"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:905
|
||||
#: documents/models.py:910
|
||||
msgid "Consumption Started"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:906
|
||||
#: documents/models.py:911
|
||||
msgid "Document Added"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:907
|
||||
#: documents/models.py:912
|
||||
msgid "Document Updated"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:910
|
||||
#: documents/models.py:915
|
||||
msgid "Consume Folder"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:911
|
||||
#: documents/models.py:916
|
||||
msgid "Api Upload"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:912
|
||||
#: documents/models.py:917
|
||||
msgid "Mail Fetch"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:915
|
||||
#: documents/models.py:920
|
||||
msgid "Workflow Trigger Type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:927
|
||||
#: documents/models.py:932
|
||||
msgid "filter path"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:932
|
||||
#: documents/models.py:937
|
||||
msgid ""
|
||||
"Only consume documents with a path that matches this if specified. Wildcards "
|
||||
"specified as * are allowed. Case insensitive."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:939
|
||||
#: documents/models.py:944
|
||||
msgid "filter filename"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:944 paperless_mail/models.py:148
|
||||
#: documents/models.py:949 paperless_mail/models.py:148
|
||||
msgid ""
|
||||
"Only consume documents which entirely match this filename if specified. "
|
||||
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:955
|
||||
#: documents/models.py:960
|
||||
msgid "filter documents from this mail rule"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:971
|
||||
#: documents/models.py:976
|
||||
msgid "has these tag(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:979
|
||||
#: documents/models.py:984
|
||||
msgid "has this document type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:987
|
||||
#: documents/models.py:992
|
||||
msgid "has this correspondent"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:991
|
||||
#: documents/models.py:996
|
||||
msgid "workflow trigger"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:992
|
||||
#: documents/models.py:997
|
||||
msgid "workflow triggers"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1002
|
||||
#: documents/models.py:1007
|
||||
msgid "Assignment"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1006
|
||||
#: documents/models.py:1011
|
||||
msgid "Removal"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1010
|
||||
#: documents/models.py:1015
|
||||
msgid "Workflow Action Type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1016
|
||||
#: documents/models.py:1021
|
||||
msgid "assign title"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1021
|
||||
#: documents/models.py:1026
|
||||
msgid ""
|
||||
"Assign a document title, can include some placeholders, see documentation."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1030 paperless_mail/models.py:216
|
||||
#: documents/models.py:1035 paperless_mail/models.py:216
|
||||
msgid "assign this tag"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1039 paperless_mail/models.py:224
|
||||
#: documents/models.py:1044 paperless_mail/models.py:224
|
||||
msgid "assign this document type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1048 paperless_mail/models.py:238
|
||||
#: documents/models.py:1053 paperless_mail/models.py:238
|
||||
msgid "assign this correspondent"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1057
|
||||
#: documents/models.py:1062
|
||||
msgid "assign this storage path"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1066
|
||||
#: documents/models.py:1071
|
||||
msgid "assign this owner"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1073
|
||||
#: documents/models.py:1078
|
||||
msgid "grant view permissions to these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1080
|
||||
#: documents/models.py:1085
|
||||
msgid "grant view permissions to these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1087
|
||||
#: documents/models.py:1092
|
||||
msgid "grant change permissions to these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1094
|
||||
#: documents/models.py:1099
|
||||
msgid "grant change permissions to these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1101
|
||||
#: documents/models.py:1106
|
||||
msgid "assign these custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1108
|
||||
#: documents/models.py:1113
|
||||
msgid "remove these tag(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1113
|
||||
#: documents/models.py:1118
|
||||
msgid "remove all tags"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1120
|
||||
#: documents/models.py:1125
|
||||
msgid "remove these document type(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1125
|
||||
#: documents/models.py:1130
|
||||
msgid "remove all document types"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1132
|
||||
#: documents/models.py:1137
|
||||
msgid "remove these correspondent(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1137
|
||||
#: documents/models.py:1142
|
||||
msgid "remove all correspondents"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1144
|
||||
#: documents/models.py:1149
|
||||
msgid "remove these storage path(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1149
|
||||
#: documents/models.py:1154
|
||||
msgid "remove all storage paths"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1156
|
||||
#: documents/models.py:1161
|
||||
msgid "remove these owner(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1161
|
||||
#: documents/models.py:1166
|
||||
msgid "remove all owners"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1168
|
||||
#: documents/models.py:1173
|
||||
msgid "remove view permissions for these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1175
|
||||
#: documents/models.py:1180
|
||||
msgid "remove view permissions for these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1182
|
||||
#: documents/models.py:1187
|
||||
msgid "remove change permissions for these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1189
|
||||
#: documents/models.py:1194
|
||||
msgid "remove change permissions for these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1194
|
||||
#: documents/models.py:1199
|
||||
msgid "remove all permissions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1201
|
||||
#: documents/models.py:1206
|
||||
msgid "remove these custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1206
|
||||
#: documents/models.py:1211
|
||||
msgid "remove all custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1210
|
||||
#: documents/models.py:1215
|
||||
msgid "workflow action"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1211
|
||||
#: documents/models.py:1216
|
||||
msgid "workflow actions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1220 paperless_mail/models.py:95
|
||||
#: documents/models.py:1225 paperless_mail/models.py:95
|
||||
msgid "order"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1226
|
||||
#: documents/models.py:1231
|
||||
msgid "triggers"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1233
|
||||
#: documents/models.py:1238
|
||||
msgid "actions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1236
|
||||
#: documents/models.py:1241
|
||||
msgid "enabled"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:114
|
||||
#: documents/serialisers.py:115
|
||||
#, python-format
|
||||
msgid "Invalid regular expression: %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:417
|
||||
#: documents/serialisers.py:418
|
||||
msgid "Invalid color."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1143
|
||||
#: documents/serialisers.py:1148
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:1252
|
||||
#: documents/serialisers.py:1257
|
||||
msgid "Invalid variable detected."
|
||||
msgstr ""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user