]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Feature: workflow removal action (#5928)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Mon, 4 Mar 2024 17:37:42 +0000 (09:37 -0800)
committerGitHub <noreply@github.com>
Mon, 4 Mar 2024 17:37:42 +0000 (17:37 +0000)
---------

Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
19 files changed:
docs/usage.md
src-ui/messages.xlf
src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts
src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
src-ui/src/app/components/common/input/permissions/permissions-group/permissions-group.component.html
src-ui/src/app/components/common/input/permissions/permissions-group/permissions-group.component.scss
src-ui/src/app/components/common/input/permissions/permissions-user/permissions-user.component.html
src-ui/src/app/components/common/input/permissions/permissions-user/permissions-user.component.scss
src-ui/src/app/components/common/input/select/select.component.scss
src-ui/src/app/data/workflow-action.ts
src/documents/consumer.py
src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py [new file with mode: 0644]
src/documents/models.py
src/documents/serialisers.py
src/documents/signals/handlers.py
src/documents/tests/test_api_workflows.py
src/documents/tests/test_workflows.py
src/locale/en_US/LC_MESSAGES/django.po

index b1b8c4cd1802231d196a3f6127b3da162a287e6f..e0335749989159e05844d0a274b1e884685aeda1 100644 (file)
@@ -329,7 +329,7 @@ Workflows allow you to filter by:
 
 ### Workflow Actions
 
-There is currently one type of workflow action, "Assignment", which can assign:
+There are currently two types of workflow actions, "Assignment", which can assign:
 
 - Title, see [title placeholders](usage.md#title-placeholders) below
 - Tags, correspondent, document type and storage path
@@ -337,6 +337,13 @@ There is currently one type of workflow action, "Assignment", which can assign:
 - View and / or edit permissions to users or groups
 - Custom fields. Note that no value for the field will be set
 
+and "Removal" actions, which can remove either all of or specific sets of the following:
+
+- Tags, correspondents, document types or storage paths
+- Document owner
+- View and / or edit permissions
+- Custom fields
+
 #### Title placeholders
 
 Workflow titles can include placeholders but the available options differ depending on the type of
index a111abc567fc7679cf1e1839ad0add1b562aae79..2c883b5adfa3b594e4cd72e553d17e59dd176944 100644 (file)
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">167</context>
+          <context context-type="linenumber">111</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">112</context>
+          <context context-type="linenumber">171</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">131</context>
+          <context context-type="linenumber">190</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">257</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">276</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">120</context>
+          <context context-type="linenumber">179</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">139</context>
+          <context context-type="linenumber">198</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">265</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">284</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">145</context>
+          <context context-type="linenumber">204</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">290</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">208</context>
+          <context context-type="linenumber">206</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</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">762</context>
+          <context context-type="linenumber">766</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</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">764</context>
+          <context context-type="linenumber">768</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">166</context>
+          <context context-type="linenumber">110</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">190</context>
+          <context context-type="linenumber">134</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6457471243969293847" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">100</context>
+          <context context-type="linenumber">159</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4754802869258527587" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">101</context>
+          <context context-type="linenumber">160</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5232720756589450549" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">164</context>
+          <context context-type="linenumber">108</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context>
           <context context-type="linenumber">72</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="3288318211116868972" datatype="html">
+        <source>Trigger type</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">118</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8727727835543352574" datatype="html">
+        <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;em&gt;"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> filters specified below.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">119</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="7467799586957602479" datatype="html">
+        <source>Filter filename</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3694878959415278689" datatype="html">
+        <source>Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">122</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="1473412958770421458" datatype="html">
+        <source>Filter sources</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">124</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="6540860478788535250" datatype="html">
+        <source>Filter path</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">125</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5491897741674893121" datatype="html">
+        <source>Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.&lt;/a&gt;</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">125</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="7468453896129193641" datatype="html">
+        <source>Filter mail rule</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8663702115863339485" datatype="html">
+        <source>Apply to documents consumed via this mail rule.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">126</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="6840369584127435743" datatype="html">
+        <source>Content matching algorithm</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">129</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="510635115034690805" datatype="html">
+        <source>Content matching pattern</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">131</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="1333789258712064056" datatype="html">
+        <source>Has tags</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">140</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5281365940563983618" datatype="html">
+        <source>Has correspondent</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">141</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="4806713133917046341" datatype="html">
+        <source>Has document type</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">142</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="6417103744331194518" datatype="html">
         <source>Action type</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">94</context>
+          <context context-type="linenumber">152</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6019822389883736115" datatype="html">
         <source>Assign title</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">98</context>
+          <context context-type="linenumber">157</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1098196422099517191" datatype="html">
         <source>Can include some placeholders, see &lt;a target=&apos;_blank&apos; href=&apos;https://docs.paperless-ngx.com/usage/#workflows&apos;&gt;documentation&lt;/a&gt;.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">98</context>
+          <context context-type="linenumber">157</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6528897010417701530" datatype="html">
         <source>Assign tags</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">99</context>
+          <context context-type="linenumber">158</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7198346314713788799" datatype="html">
         <source>Assign storage path</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">102</context>
+          <context context-type="linenumber">161</context>
         </context-group>
       </trans-unit>
       <trans-unit id="475685412372379925" datatype="html">
         <source>Assign custom fields</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">103</context>
+          <context context-type="linenumber">162</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5057200219587080996" datatype="html">
         <source>Assign owner</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">106</context>
+          <context context-type="linenumber">165</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1749184201773078639" datatype="html">
         <source>Assign view permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">108</context>
+          <context context-type="linenumber">167</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1744964187586405039" datatype="html">
         <source>Assign edit permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">127</context>
+          <context context-type="linenumber">186</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="3288318211116868972" datatype="html">
-        <source>Trigger type</source>
+      <trans-unit id="6236311670364192011" datatype="html">
+        <source>Remove tags</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">174</context>
+          <context context-type="linenumber">213</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="8727727835543352574" datatype="html">
-        <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;em&gt;"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> filters specified below.</source>
+      <trans-unit id="7890599006071681081" datatype="html">
+        <source>Remove all</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">175</context>
+          <context context-type="linenumber">214</context>
         </context-group>
-      </trans-unit>
-      <trans-unit id="7467799586957602479" datatype="html">
-        <source>Filter filename</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">178</context>
+          <context context-type="linenumber">220</context>
         </context-group>
-      </trans-unit>
-      <trans-unit id="3694878959415278689" datatype="html">
-        <source>Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">178</context>
+          <context context-type="linenumber">226</context>
         </context-group>
-      </trans-unit>
-      <trans-unit id="1473412958770421458" datatype="html">
-        <source>Filter sources</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">180</context>
+          <context context-type="linenumber">232</context>
         </context-group>
-      </trans-unit>
-      <trans-unit id="6540860478788535250" datatype="html">
-        <source>Filter path</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">181</context>
+          <context context-type="linenumber">238</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">245</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+          <context context-type="linenumber">251</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="5491897741674893121" datatype="html">
-        <source>Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.&lt;/a&gt;</source>
+      <trans-unit id="8636414563726517994" datatype="html">
+        <source>Remove correspondents</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">181</context>
+          <context context-type="linenumber">219</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="7468453896129193641" datatype="html">
-        <source>Filter mail rule</source>
+      <trans-unit id="5305293055593064952" datatype="html">
+        <source>Remove document types</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">182</context>
+          <context context-type="linenumber">225</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="8663702115863339485" datatype="html">
-        <source>Apply to documents consumed via this mail rule.</source>
+      <trans-unit id="2400388879708187" datatype="html">
+        <source>Remove storage paths</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">182</context>
+          <context context-type="linenumber">231</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="6840369584127435743" datatype="html">
-        <source>Content matching algorithm</source>
+      <trans-unit id="4324304327041955720" datatype="html">
+        <source>Remove custom fields</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">185</context>
+          <context context-type="linenumber">237</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="510635115034690805" datatype="html">
-        <source>Content matching pattern</source>
+      <trans-unit id="8367536502602515064" datatype="html">
+        <source>Remove owners</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">187</context>
+          <context context-type="linenumber">244</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="1333789258712064056" datatype="html">
-        <source>Has tags</source>
+      <trans-unit id="3393772184866313281" datatype="html">
+        <source>Remove permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">196</context>
+          <context context-type="linenumber">250</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="5281365940563983618" datatype="html">
-        <source>Has correspondent</source>
+      <trans-unit id="3145629643370481114" datatype="html">
+        <source>View permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">197</context>
+          <context context-type="linenumber">253</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="4806713133917046341" datatype="html">
-        <source>Has document type</source>
+      <trans-unit id="1946660694635960249" datatype="html">
+        <source>Edit permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
-          <context context-type="linenumber">198</context>
+          <context context-type="linenumber">272</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4626030417479279989" datatype="html">
           <context context-type="linenumber">69</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="6234812824772766804" datatype="html">
+        <source>Removal</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
+          <context context-type="linenumber">73</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="3138206142174978019" datatype="html">
         <source>Create new workflow</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
-          <context context-type="linenumber">137</context>
+          <context context-type="linenumber">142</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5996779210524133604" datatype="html">
         <source>Edit workflow</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
-          <context context-type="linenumber">141</context>
+          <context context-type="linenumber">146</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1616102757855967475" 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">637</context>
+          <context context-type="linenumber">638</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">646</context>
+          <context context-type="linenumber">649</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">650</context>
+          <context context-type="linenumber">653</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">691</context>
+          <context context-type="linenumber">694</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">717</context>
+          <context context-type="linenumber">721</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">204</context>
+          <context context-type="linenumber">202</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
         <source>Do you really want to delete document &quot;<x id="PH" equiv-text="this.document.title"/>&quot;?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
-          <context context-type="linenumber">718</context>
+          <context context-type="linenumber">722</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">719</context>
+          <context context-type="linenumber">723</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">721</context>
+          <context context-type="linenumber">725</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">740</context>
+          <context context-type="linenumber">744</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">760</context>
+          <context context-type="linenumber">764</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
         <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">761</context>
+          <context context-type="linenumber">765</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">772</context>
+          <context context-type="linenumber">776</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">783</context>
+          <context context-type="linenumber">787</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">852</context>
+          <context context-type="linenumber">856</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6857598786757174736" datatype="html">
         <source>Automatic</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">116</context>
+          <context context-type="linenumber">114</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/matching-model.ts</context>
         <source>None</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">118</context>
+          <context context-type="linenumber">116</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/matching-model.ts</context>
         <source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">161</context>
+          <context context-type="linenumber">159</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3928835053823658072" datatype="html">
         <source>Error occurred while creating <x id="PH" equiv-text="this.typeName"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">166</context>
+          <context context-type="linenumber">164</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2541368547549828690" datatype="html">
         <source>Successfully updated <x id="PH" equiv-text="this.typeName"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">181</context>
+          <context context-type="linenumber">179</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6442673774206210733" datatype="html">
         <source>Error occurred while saving <x id="PH" equiv-text="this.typeName"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">186</context>
+          <context context-type="linenumber">184</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8371896857609524947" datatype="html">
         <source>Associated documents will not be deleted.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">206</context>
+          <context context-type="linenumber">204</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6639207128255974941" datatype="html">
         <source>Error while deleting element</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">222</context>
+          <context context-type="linenumber">220</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4863024195229581844" datatype="html">
index 623119605c9d0fff1790caff54bcbe4fe114c8c6..4134a1fb95d3f54172750cec224973dd6aef62bb 100644 (file)
                     </div>
                     <div ngbAccordionCollapse>
                       <div ngbAccordionBody>
-                        <pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
-                        <input type="hidden" formControlName="id" />
-                        <div class="row">
-                          <div class="col">
-                            <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
-                            <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
-                            <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
-                            <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
-                            <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
-                            <pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
-                          </div>
-                          <div class="col">
-                            <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
-                            <div>
-                              <label class="form-label" i18n>Assign view permissions</label>
-                              <div class="mb-2">
-                                <div class="row mb-1">
-                                  <div class="col-lg-3">
-                                    <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
-                                  </div>
-                                  <div class="col-lg-9">
-                                    <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
-                                  </div>
-                                </div>
-                                <div class="row">
-                                  <div class="col-lg-3">
-                                    <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
-                                  </div>
-                                  <div class="col-lg-9">
-                                    <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
-                                  </div>
-                                </div>
-                              </div>
-                              <label class="form-label" i18n>Assign edit permissions</label>
-                              <div>
-                                <div class="row mb-1">
-                                  <div class="col-lg-3">
-                                    <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
-                                  </div>
-                                  <div class="col-lg-9">
-                                    <pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
-                                  </div>
-                                </div>
-                                <div class="row">
-                                  <div class="col-lg-3">
-                                    <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
-                                  </div>
-                                  <div class="col-lg-9">
-                                    <pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
-                                  </div>
-                                </div>
-                                <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
-                              </div>
-                            </div>
-                          </div>
-                        </div>
-
+                        <ng-template [ngTemplateOutlet]="actionForm" [ngTemplateOutletContext]="{ formGroup: actionFields.controls[i], action: action }"></ng-template>
                       </div>
                     </div>
                   </div>
     </div>
   </div>
 </ng-template>
+
+<ng-template #actionForm let-formGroup="formGroup" let action="action">
+  <div [formGroup]="formGroup">
+    <input type="hidden" formControlName="id" />
+    <pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
+    @switch(formGroup.get('type').value) {
+      @case ( WorkflowActionType.Assignment) {
+        <div class="row">
+          <div class="col">
+            <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
+            <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
+            <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
+            <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
+            <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
+            <pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
+          </div>
+          <div class="col">
+            <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
+            <div>
+              <label class="form-label" i18n>Assign view permissions</label>
+              <div class="mb-2">
+                <div class="row mb-1">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
+                  </div>
+                </div>
+              </div>
+              <label class="form-label" i18n>Assign edit permissions</label>
+              <div>
+                <div class="row mb-1">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
+                  </div>
+                </div>
+                <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
+              </div>
+            </div>
+          </div>
+        </div>
+      }
+      @case (WorkflowActionType.Removal) {
+        <div class="row">
+          <div class="col">
+            <h6 class="form-label" i18n>Remove tags</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_tags"></pngx-input-switch>
+            <div class="mt-n3">
+              <pngx-input-tags [allowCreate]="false" title="" formControlName="remove_tags"></pngx-input-tags>
+            </div>
+
+            <h6 class="form-label" i18n>Remove correspondents</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_correspondents"></pngx-input-switch>
+            <div class="mt-n3">
+              <pngx-input-select i18n-title title="" multiple="true" [items]="correspondents" formControlName="remove_correspondents"></pngx-input-select>
+            </div>
+
+            <h6 class="form-label" i18n>Remove document types</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_document_types"></pngx-input-switch>
+            <div class="mt-n3">
+              <pngx-input-select i18n-title title="" multiple="true" [items]="documentTypes" formControlName="remove_document_types"></pngx-input-select>
+            </div>
+
+            <h6 class="form-label" i18n>Remove storage paths</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_storage_paths"></pngx-input-switch>
+            <div class="mt-n3">
+              <pngx-input-select i18n-title title="" multiple="true" [items]="storagePaths" formControlName="remove_storage_paths"></pngx-input-select>
+            </div>
+
+            <h6 class="form-label" i18n>Remove custom fields</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_custom_fields"></pngx-input-switch>
+            <div class="mt-n3">
+              <pngx-input-select i18n-title title="" multiple="true" [items]="customFields" formControlName="remove_custom_fields"></pngx-input-select>
+            </div>
+          </div>
+          <div class="col">
+            <h6 class="form-label" i18n>Remove owners</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_owners"></pngx-input-switch>
+            <div class="mt-n3">
+              <pngx-input-select i18n-title title="" multiple="true" [items]="users" bindLabel="username" formControlName="remove_owners"></pngx-input-select>
+            </div>
+
+            <h6 class="form-label" i18n>Remove permissions</h6>
+            <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_permissions"></pngx-input-switch>
+            <div>
+              <label class="form-label" i18n>View permissions</label>
+              <div class="mb-2">
+                <div class="row mb-1">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-user type="view" formControlName="remove_view_users"></pngx-permissions-user>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-group type="view" formControlName="remove_view_groups"></pngx-permissions-group>
+                  </div>
+                </div>
+              </div>
+              <label class="form-label" i18n>Edit permissions</label>
+              <div>
+                <div class="row mb-1">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-user type="change" formControlName="remove_change_users"></pngx-permissions-user>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col-lg-3">
+                    <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+                  </div>
+                  <div class="col-lg-9">
+                    <pngx-permissions-group type="change" formControlName="remove_change_groups"></pngx-permissions-group>
+                  </div>
+                </div>
+                <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
+              </div>
+            </div>
+          </div>
+        </div>
+      }
+    }
+  </div>
+</ng-template>
index f7eeb1bf031fc0288de520cde3f5e8d4e937b6b6..8d8ef6850ebaa7054c7cc760513c7630b02dea89 100644 (file)
@@ -235,4 +235,103 @@ describe('WorkflowEditDialogComponent', () => {
       MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
     )
   })
+
+  it('should disable or enable action fields based on removal action type', () => {
+    const workflow: Workflow = {
+      name: 'Workflow 1',
+      id: 1,
+      order: 1,
+      enabled: true,
+      triggers: [],
+      actions: [
+        {
+          id: 1,
+          type: WorkflowActionType.Removal,
+          remove_all_tags: true,
+          remove_all_document_types: true,
+          remove_all_correspondents: true,
+          remove_all_storage_paths: true,
+          remove_all_custom_fields: true,
+          remove_all_owners: true,
+          remove_all_permissions: true,
+        },
+      ],
+    }
+    component.object = workflow
+    component.ngOnInit()
+
+    component['checkRemovalActionFields'](workflow)
+
+    // Assert that the action fields are disabled or enabled correctly
+    expect(
+      component.actionFields.at(0).get('remove_tags').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_document_types').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_correspondents').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_storage_paths').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_custom_fields').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_owners').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_view_users').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_view_groups').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_change_users').disabled
+    ).toBeTruthy()
+    expect(
+      component.actionFields.at(0).get('remove_change_groups').disabled
+    ).toBeTruthy()
+
+    workflow.actions[0].remove_all_tags = false
+    workflow.actions[0].remove_all_document_types = false
+    workflow.actions[0].remove_all_correspondents = false
+    workflow.actions[0].remove_all_storage_paths = false
+    workflow.actions[0].remove_all_custom_fields = false
+    workflow.actions[0].remove_all_owners = false
+    workflow.actions[0].remove_all_permissions = false
+
+    component['checkRemovalActionFields'](workflow)
+
+    // Assert that the action fields are disabled or enabled correctly
+    expect(component.actionFields.at(0).get('remove_tags').disabled).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_document_types').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_correspondents').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_storage_paths').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_custom_fields').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_owners').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_view_users').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_view_groups').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_change_users').disabled
+    ).toBeFalsy()
+    expect(
+      component.actionFields.at(0).get('remove_change_groups').disabled
+    ).toBeFalsy()
+  })
 })
index 9bf49036f6c7e5d3d523daaf683f95e099749129..77e079fd260d90ddd12611746d29581025b06114 100644 (file)
@@ -68,6 +68,10 @@ export const WORKFLOW_ACTION_OPTIONS = [
     id: WorkflowActionType.Assignment,
     name: $localize`Assignment`,
   },
+  {
+    id: WorkflowActionType.Removal,
+    name: $localize`Removal`,
+  },
 ]
 
 const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
@@ -84,6 +88,7 @@ export class WorkflowEditDialogComponent
   implements OnInit
 {
   public WorkflowTriggerType = WorkflowTriggerType
+  public WorkflowActionType = WorkflowActionType
 
   templates: Workflow[]
   correspondents: Correspondent[]
@@ -159,6 +164,124 @@ export class WorkflowEditDialogComponent
   ngOnInit(): void {
     super.ngOnInit()
     this.updateAllTriggerActionFields()
+    this.objectForm.valueChanges.subscribe(
+      this.checkRemovalActionFields.bind(this)
+    )
+    this.checkRemovalActionFields(this.objectForm.value)
+  }
+
+  private checkRemovalActionFields(formWorkflow: Workflow) {
+    formWorkflow.actions
+      .filter((action) => action.type === WorkflowActionType.Removal)
+      .forEach((action, i) => {
+        if (action.remove_all_tags) {
+          this.actionFields
+            .at(i)
+            .get('remove_tags')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_tags')
+            .enable({ emitEvent: false })
+        }
+
+        if (action.remove_all_document_types) {
+          this.actionFields
+            .at(i)
+            .get('remove_document_types')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_document_types')
+            .enable({ emitEvent: false })
+        }
+
+        if (action.remove_all_correspondents) {
+          this.actionFields
+            .at(i)
+            .get('remove_correspondents')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_correspondents')
+            .enable({ emitEvent: false })
+        }
+
+        if (action.remove_all_storage_paths) {
+          this.actionFields
+            .at(i)
+            .get('remove_storage_paths')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_storage_paths')
+            .enable({ emitEvent: false })
+        }
+
+        if (action.remove_all_custom_fields) {
+          this.actionFields
+            .at(i)
+            .get('remove_custom_fields')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_custom_fields')
+            .enable({ emitEvent: false })
+        }
+
+        if (action.remove_all_owners) {
+          this.actionFields
+            .at(i)
+            .get('remove_owners')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_owners')
+            .enable({ emitEvent: false })
+        }
+
+        if (action.remove_all_permissions) {
+          this.actionFields
+            .at(i)
+            .get('remove_view_users')
+            .disable({ emitEvent: false })
+          this.actionFields
+            .at(i)
+            .get('remove_view_groups')
+            .disable({ emitEvent: false })
+          this.actionFields
+            .at(i)
+            .get('remove_change_users')
+            .disable({ emitEvent: false })
+          this.actionFields
+            .at(i)
+            .get('remove_change_groups')
+            .disable({ emitEvent: false })
+        } else {
+          this.actionFields
+            .at(i)
+            .get('remove_view_users')
+            .enable({ emitEvent: false })
+          this.actionFields
+            .at(i)
+            .get('remove_view_groups')
+            .enable({ emitEvent: false })
+          this.actionFields
+            .at(i)
+            .get('remove_change_users')
+            .enable({ emitEvent: false })
+          this.actionFields
+            .at(i)
+            .get('remove_change_groups')
+            .enable({ emitEvent: false })
+        }
+      })
   }
 
   get triggerFields(): FormArray {
@@ -215,6 +338,31 @@ export class WorkflowEditDialogComponent
         assign_change_users: new FormControl(action.assign_change_users),
         assign_change_groups: new FormControl(action.assign_change_groups),
         assign_custom_fields: new FormControl(action.assign_custom_fields),
+        remove_tags: new FormControl(action.remove_tags),
+        remove_all_tags: new FormControl(action.remove_all_tags),
+        remove_document_types: new FormControl(action.remove_document_types),
+        remove_all_document_types: new FormControl(
+          action.remove_all_document_types
+        ),
+        remove_correspondents: new FormControl(action.remove_correspondents),
+        remove_all_correspondents: new FormControl(
+          action.remove_all_correspondents
+        ),
+        remove_storage_paths: new FormControl(action.remove_storage_paths),
+        remove_all_storage_paths: new FormControl(
+          action.remove_all_storage_paths
+        ),
+        remove_owners: new FormControl(action.remove_owners),
+        remove_all_owners: new FormControl(action.remove_all_owners),
+        remove_view_users: new FormControl(action.remove_view_users),
+        remove_view_groups: new FormControl(action.remove_view_groups),
+        remove_change_users: new FormControl(action.remove_change_users),
+        remove_change_groups: new FormControl(action.remove_change_groups),
+        remove_all_permissions: new FormControl(action.remove_all_permissions),
+        remove_custom_fields: new FormControl(action.remove_custom_fields),
+        remove_all_custom_fields: new FormControl(
+          action.remove_all_custom_fields
+        ),
       }),
       { emitEvent }
     )
@@ -290,6 +438,23 @@ export class WorkflowEditDialogComponent
       assign_change_users: [],
       assign_change_groups: [],
       assign_custom_fields: [],
+      remove_tags: [],
+      remove_all_tags: false,
+      remove_document_types: [],
+      remove_all_document_types: false,
+      remove_correspondents: [],
+      remove_all_correspondents: false,
+      remove_storage_paths: [],
+      remove_all_storage_paths: false,
+      remove_owners: [],
+      remove_all_owners: false,
+      remove_view_users: [],
+      remove_view_groups: [],
+      remove_change_users: [],
+      remove_change_groups: [],
+      remove_all_permissions: false,
+      remove_custom_fields: [],
+      remove_all_custom_fields: false,
     }
     this.object.actions.push(action)
     this.createActionField(action)
index 5ff0fa894a41849f359336c37698c94f0e94962f..6cf5689a9c934a99f547ccf705f602116d8b1c11 100644 (file)
@@ -1,4 +1,4 @@
-<div class="paperless-input-select">
+<div class="paperless-input-select" [class.disabled]="disabled">
     <div>
       <ng-select name="inputId" [(ngModel)]="value"
         [disabled]="disabled"
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..822f7e490500ea7a5e0d141658295d36bcbbda53 100644 (file)
@@ -0,0 +1,11 @@
+.paperless-input-select.disabled {
+    cursor: not-allowed;
+
+    ::ng-deep ng-select {
+        pointer-events: none;
+
+        .ng-select-container {
+            background-color: var(--pngx-bg-alt) !important;
+        }
+    }
+}
index fdb832cf124dd7b210e7279878d6024acd54c27b..9e0e2025dbb247a562627af69b1030d5fc16c8b5 100644 (file)
@@ -1,4 +1,4 @@
-<div class="paperless-input-select">
+<div class="paperless-input-select" [class.disabled]="disabled">
   <div>
     <ng-select name="inputId" [(ngModel)]="value"
       [disabled]="disabled"
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..822f7e490500ea7a5e0d141658295d36bcbbda53 100644 (file)
@@ -0,0 +1,11 @@
+.paperless-input-select.disabled {
+    cursor: not-allowed;
+
+    ::ng-deep ng-select {
+        pointer-events: none;
+
+        .ng-select-container {
+            background-color: var(--pngx-bg-alt) !important;
+        }
+    }
+}
index 7cfe14fdc83da5acce2181fe5162e31e6150bc94..31466423f574b5a8918e11d6c3ed8fbd28465fb8 100644 (file)
@@ -1,6 +1,7 @@
 // styles for ng-select child are in styles.scss
 .paperless-input-select.disabled {
-    .input-group {
+    .input-group,
+    div > div {
         cursor: not-allowed;
     }
 
index a0da5f03a679cecf93e25d0f186d180d858c0934..ff64d19b3b3c1285afc61c0aca7a16280e6d5a63 100644 (file)
@@ -2,6 +2,7 @@ import { ObjectWithId } from './object-with-id'
 
 export enum WorkflowActionType {
   Assignment = 1,
+  Removal = 2,
 }
 export interface WorkflowAction extends ObjectWithId {
   type: WorkflowActionType
@@ -27,4 +28,38 @@ export interface WorkflowAction extends ObjectWithId {
   assign_change_groups?: number[] // [Group.id]
 
   assign_custom_fields?: number[] // [CustomField.id]
+
+  remove_tags?: number[] // Tag.id
+
+  remove_all_tags?: boolean
+
+  remove_document_types?: number[] // [DocumentType.id]
+
+  remove_all_document_types?: boolean
+
+  remove_correspondents?: number[] // [Correspondent.id]
+
+  remove_all_correspondents?: boolean
+
+  remove_storage_paths?: number[] // [StoragePath.id]
+
+  remove_all_storage_paths?: boolean
+
+  remove_owners?: number[] // [User.id]
+
+  remove_all_owners?: boolean
+
+  remove_view_users?: number[] // [User.id]
+
+  remove_view_groups?: number[] // [Group.id]
+
+  remove_change_users?: number[] // [User.id]
+
+  remove_change_groups?: number[] // [Group.id]
+
+  remove_all_permissions?: boolean
+
+  remove_custom_fields?: number[] // [CustomField.id]
+
+  remove_all_custom_fields?: boolean
 }
index 93b41e60ee25d0fd6160b946e178b2a9efb94fd2..3b783cae9f55980dbd783c9b1a4e5be287d00a30 100644 (file)
@@ -7,6 +7,7 @@ from enum import Enum
 from pathlib import Path
 from subprocess import CompletedProcess
 from subprocess import run
+from typing import TYPE_CHECKING
 from typing import Optional
 
 import magic
@@ -35,6 +36,7 @@ from documents.models import FileInfo
 from documents.models import StoragePath
 from documents.models import Tag
 from documents.models import Workflow
+from documents.models import WorkflowAction
 from documents.models import WorkflowTrigger
 from documents.parsers import DocumentParser
 from documents.parsers import ParseError
@@ -63,9 +65,26 @@ class WorkflowTriggerPlugin(
         """
         Get overrides from matching workflows
         """
+        msg = ""
         overrides = DocumentMetadataOverrides()
-        for workflow in Workflow.objects.filter(enabled=True).order_by("order"):
-            template_overrides = DocumentMetadataOverrides()
+        for workflow in (
+            Workflow.objects.filter(enabled=True)
+            .prefetch_related("actions")
+            .prefetch_related("actions__assign_view_users")
+            .prefetch_related("actions__assign_view_groups")
+            .prefetch_related("actions__assign_change_users")
+            .prefetch_related("actions__assign_change_groups")
+            .prefetch_related("actions__assign_custom_fields")
+            .prefetch_related("actions__remove_tags")
+            .prefetch_related("actions__remove_correspondents")
+            .prefetch_related("actions__remove_document_types")
+            .prefetch_related("actions__remove_storage_paths")
+            .prefetch_related("actions__remove_custom_fields")
+            .prefetch_related("actions__remove_owners")
+            .prefetch_related("triggers")
+            .order_by("order")
+        ):
+            action_overrides = DocumentMetadataOverrides()
 
             if document_matches_workflow(
                 self.input_doc,
@@ -73,49 +92,137 @@ class WorkflowTriggerPlugin(
                 WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
             ):
                 for action in workflow.actions.all():
-                    if action.assign_title is not None:
-                        template_overrides.title = action.assign_title
-                    if action.assign_tags is not None:
-                        template_overrides.tag_ids = [
-                            tag.pk for tag in action.assign_tags.all()
-                        ]
-                    if action.assign_correspondent is not None:
-                        template_overrides.correspondent_id = (
-                            action.assign_correspondent.pk
-                        )
-                    if action.assign_document_type is not None:
-                        template_overrides.document_type_id = (
-                            action.assign_document_type.pk
-                        )
-                    if action.assign_storage_path is not None:
-                        template_overrides.storage_path_id = (
-                            action.assign_storage_path.pk
-                        )
-                    if action.assign_owner is not None:
-                        template_overrides.owner_id = action.assign_owner.pk
-                    if action.assign_view_users is not None:
-                        template_overrides.view_users = [
-                            user.pk for user in action.assign_view_users.all()
-                        ]
-                    if action.assign_view_groups is not None:
-                        template_overrides.view_groups = [
-                            group.pk for group in action.assign_view_groups.all()
-                        ]
-                    if action.assign_change_users is not None:
-                        template_overrides.change_users = [
-                            user.pk for user in action.assign_change_users.all()
-                        ]
-                    if action.assign_change_groups is not None:
-                        template_overrides.change_groups = [
-                            group.pk for group in action.assign_change_groups.all()
-                        ]
-                    if action.assign_custom_fields is not None:
-                        template_overrides.custom_field_ids = [
-                            field.pk for field in action.assign_custom_fields.all()
-                        ]
-
-                    overrides.update(template_overrides)
+                    if TYPE_CHECKING:
+                        assert isinstance(action, WorkflowAction)
+                    msg += f"Applying {action} from {workflow}\n"
+                    if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT:
+                        if action.assign_title is not None:
+                            action_overrides.title = action.assign_title
+                        if action.assign_tags is not None:
+                            action_overrides.tag_ids = list(
+                                action.assign_tags.values_list("pk", flat=True),
+                            )
+
+                        if action.assign_correspondent is not None:
+                            action_overrides.correspondent_id = (
+                                action.assign_correspondent.pk
+                            )
+                        if action.assign_document_type is not None:
+                            action_overrides.document_type_id = (
+                                action.assign_document_type.pk
+                            )
+                        if action.assign_storage_path is not None:
+                            action_overrides.storage_path_id = (
+                                action.assign_storage_path.pk
+                            )
+                        if action.assign_owner is not None:
+                            action_overrides.owner_id = action.assign_owner.pk
+                        if action.assign_view_users is not None:
+                            action_overrides.view_users = list(
+                                action.assign_view_users.values_list("pk", flat=True),
+                            )
+                        if action.assign_view_groups is not None:
+                            action_overrides.view_groups = list(
+                                action.assign_view_groups.values_list("pk", flat=True),
+                            )
+                        if action.assign_change_users is not None:
+                            action_overrides.change_users = list(
+                                action.assign_change_users.values_list("pk", flat=True),
+                            )
+                        if action.assign_change_groups is not None:
+                            action_overrides.change_groups = list(
+                                action.assign_change_groups.values_list(
+                                    "pk",
+                                    flat=True,
+                                ),
+                            )
+                        if action.assign_custom_fields is not None:
+                            action_overrides.custom_field_ids = list(
+                                action.assign_custom_fields.values_list(
+                                    "pk",
+                                    flat=True,
+                                ),
+                            )
+                        overrides.update(action_overrides)
+                    elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
+                        # Removal actions overwrite the current overrides
+                        if action.remove_all_tags:
+                            overrides.tag_ids = []
+                        elif overrides.tag_ids:
+                            for tag in action.remove_custom_fields.filter(
+                                pk__in=overrides.tag_ids,
+                            ):
+                                overrides.tag_ids.remove(tag.pk)
+
+                        if action.remove_all_correspondents or (
+                            overrides.correspondent_id is not None
+                            and action.remove_correspondents.filter(
+                                pk=overrides.correspondent_id,
+                            ).exists()
+                        ):
+                            overrides.correspondent_id = None
+
+                        if action.remove_all_document_types or (
+                            overrides.document_type_id is not None
+                            and action.remove_document_types.filter(
+                                pk=overrides.document_type_id,
+                            ).exists()
+                        ):
+                            overrides.document_type_id = None
+
+                        if action.remove_all_storage_paths or (
+                            overrides.storage_path_id is not None
+                            and action.remove_storage_paths.filter(
+                                pk=overrides.storage_path_id,
+                            ).exists()
+                        ):
+                            overrides.storage_path_id = None
+
+                        if action.remove_all_custom_fields:
+                            overrides.custom_field_ids = []
+                        elif overrides.custom_field_ids:
+                            for field in action.remove_custom_fields.filter(
+                                pk__in=overrides.custom_field_ids,
+                            ):
+                                overrides.custom_field_ids.remove(field.pk)
+
+                        if action.remove_all_owners or (
+                            overrides.owner_id is not None
+                            and action.remove_owners.filter(
+                                pk=overrides.owner_id,
+                            ).exists()
+                        ):
+                            overrides.owner_id = None
+
+                        if action.remove_all_permissions:
+                            overrides.view_users = []
+                            overrides.view_groups = []
+                            overrides.change_users = []
+                            overrides.change_groups = []
+                        else:
+                            if overrides.view_users:
+                                for user in action.remove_view_users.filter(
+                                    pk__in=overrides.view_users,
+                                ):
+                                    overrides.view_users.remove(user.pk)
+                            if overrides.change_users:
+                                for user in action.remove_change_users.filter(
+                                    pk__in=overrides.change_users,
+                                ):
+                                    overrides.change_users.remove(user.pk)
+                            if overrides.view_groups:
+                                for user in action.remove_view_groups.filter(
+                                    pk__in=overrides.view_groups,
+                                ):
+                                    overrides.view_groups.remove(user.pk)
+                            if overrides.change_groups:
+                                for user in action.remove_change_groups.filter(
+                                    pk__in=overrides.change_groups,
+                                ):
+                                    overrides.change_groups.remove(user.pk)
+
         self.metadata.update(overrides)
+        return msg
 
 
 class ConsumerError(Exception):
diff --git a/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py b/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py
new file mode 100644 (file)
index 0000000..6ce5da9
--- /dev/null
@@ -0,0 +1,223 @@
+# Generated by Django 4.2.10 on 2024-02-21 21:19
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("auth", "0012_alter_user_first_name_max_length"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("documents", "1045_alter_customfieldinstance_value_monetary"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_correspondents",
+            field=models.BooleanField(
+                default=False,
+                verbose_name="remove all correspondents",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_custom_fields",
+            field=models.BooleanField(
+                default=False,
+                verbose_name="remove all custom fields",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_document_types",
+            field=models.BooleanField(
+                default=False,
+                verbose_name="remove all document types",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_owners",
+            field=models.BooleanField(default=False, verbose_name="remove all owners"),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_permissions",
+            field=models.BooleanField(
+                default=False,
+                verbose_name="remove all permissions",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_storage_paths",
+            field=models.BooleanField(
+                default=False,
+                verbose_name="remove all storage paths",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_all_tags",
+            field=models.BooleanField(default=False, verbose_name="remove all tags"),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_change_groups",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="auth.group",
+                verbose_name="remove change permissions for these groups",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_change_users",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="remove change permissions for these users",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_correspondents",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.correspondent",
+                verbose_name="remove these correspondent(s)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_custom_fields",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.customfield",
+                verbose_name="remove these custom fields",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_document_types",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.documenttype",
+                verbose_name="remove these document type(s)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_owners",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="remove these owner(s)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_storage_paths",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.storagepath",
+                verbose_name="remove these storage path(s)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_tags",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.tag",
+                verbose_name="remove these tag(s)",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_view_groups",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="auth.group",
+                verbose_name="remove view permissions for these groups",
+            ),
+        ),
+        migrations.AddField(
+            model_name="workflowaction",
+            name="remove_view_users",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to=settings.AUTH_USER_MODEL,
+                verbose_name="remove view permissions for these users",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="workflowaction",
+            name="assign_correspondent",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="+",
+                to="documents.correspondent",
+                verbose_name="assign this correspondent",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="workflowaction",
+            name="assign_document_type",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="+",
+                to="documents.documenttype",
+                verbose_name="assign this document type",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="workflowaction",
+            name="assign_storage_path",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.SET_NULL,
+                related_name="+",
+                to="documents.storagepath",
+                verbose_name="assign this storage path",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="workflowaction",
+            name="assign_tags",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.tag",
+                verbose_name="assign this tag",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="workflowaction",
+            name="type",
+            field=models.PositiveIntegerField(
+                choices=[(1, "Assignment"), (2, "Removal")],
+                default=1,
+                verbose_name="Workflow Action Type",
+            ),
+        ),
+    ]
index 6dc24c801ac1b1c4796a9c40c934fbf3ceb65b5f..8e7a16a604bcf4efa4f1987de889818b7e7be618 100644 (file)
@@ -997,7 +997,14 @@ class WorkflowTrigger(models.Model):
 
 class WorkflowAction(models.Model):
     class WorkflowActionType(models.IntegerChoices):
-        ASSIGNMENT = 1, _("Assignment")
+        ASSIGNMENT = (
+            1,
+            _("Assignment"),
+        )
+        REMOVAL = (
+            2,
+            _("Removal"),
+        )
 
     type = models.PositiveIntegerField(
         _("Workflow Action Type"),
@@ -1019,6 +1026,7 @@ class WorkflowAction(models.Model):
     assign_tags = models.ManyToManyField(
         Tag,
         blank=True,
+        related_name="+",
         verbose_name=_("assign this tag"),
     )
 
@@ -1027,6 +1035,7 @@ class WorkflowAction(models.Model):
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
+        related_name="+",
         verbose_name=_("assign this document type"),
     )
 
@@ -1035,6 +1044,7 @@ class WorkflowAction(models.Model):
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
+        related_name="+",
         verbose_name=_("assign this correspondent"),
     )
 
@@ -1043,6 +1053,7 @@ class WorkflowAction(models.Model):
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
+        related_name="+",
         verbose_name=_("assign this storage path"),
     )
 
@@ -1090,6 +1101,111 @@ class WorkflowAction(models.Model):
         verbose_name=_("assign these custom fields"),
     )
 
+    remove_tags = models.ManyToManyField(
+        Tag,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these tag(s)"),
+    )
+
+    remove_all_tags = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all tags"),
+    )
+
+    remove_document_types = models.ManyToManyField(
+        DocumentType,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these document type(s)"),
+    )
+
+    remove_all_document_types = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all document types"),
+    )
+
+    remove_correspondents = models.ManyToManyField(
+        Correspondent,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these correspondent(s)"),
+    )
+
+    remove_all_correspondents = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all correspondents"),
+    )
+
+    remove_storage_paths = models.ManyToManyField(
+        StoragePath,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these storage path(s)"),
+    )
+
+    remove_all_storage_paths = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all storage paths"),
+    )
+
+    remove_owners = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these owner(s)"),
+    )
+
+    remove_all_owners = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all owners"),
+    )
+
+    remove_view_users = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove view permissions for these users"),
+    )
+
+    remove_view_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove view permissions for these groups"),
+    )
+
+    remove_change_users = models.ManyToManyField(
+        User,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove change permissions for these users"),
+    )
+
+    remove_change_groups = models.ManyToManyField(
+        Group,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove change permissions for these groups"),
+    )
+
+    remove_all_permissions = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all permissions"),
+    )
+
+    remove_custom_fields = models.ManyToManyField(
+        CustomField,
+        blank=True,
+        related_name="+",
+        verbose_name=_("remove these custom fields"),
+    )
+
+    remove_all_custom_fields = models.BooleanField(
+        default=False,
+        verbose_name=_("remove all custom fields"),
+    )
+
     class Meta:
         verbose_name = _("workflow action")
         verbose_name_plural = _("workflow actions")
index adcb0d251aa7018cc7e367ce67e1ad4fcc97b8a5..5ea0e21c8d1c07b403e099eccc624548efe8b9dd 100644 (file)
@@ -1471,6 +1471,23 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
             "assign_change_users",
             "assign_change_groups",
             "assign_custom_fields",
+            "remove_all_tags",
+            "remove_tags",
+            "remove_all_correspondents",
+            "remove_correspondents",
+            "remove_all_document_types",
+            "remove_document_types",
+            "remove_all_storage_paths",
+            "remove_storage_paths",
+            "remove_custom_fields",
+            "remove_all_custom_fields",
+            "remove_all_owners",
+            "remove_owners",
+            "remove_all_permissions",
+            "remove_view_users",
+            "remove_view_groups",
+            "remove_change_users",
+            "remove_change_groups",
         ]
 
     def validate(self, attrs):
@@ -1551,10 +1568,22 @@ class WorkflowSerializer(serializers.ModelSerializer):
                 assign_change_users = action.pop("assign_change_users", None)
                 assign_change_groups = action.pop("assign_change_groups", None)
                 assign_custom_fields = action.pop("assign_custom_fields", None)
+                remove_tags = action.pop("remove_tags", None)
+                remove_correspondents = action.pop("remove_correspondents", None)
+                remove_document_types = action.pop("remove_document_types", None)
+                remove_storage_paths = action.pop("remove_storage_paths", None)
+                remove_custom_fields = action.pop("remove_custom_fields", None)
+                remove_owners = action.pop("remove_owners", None)
+                remove_view_users = action.pop("remove_view_users", None)
+                remove_view_groups = action.pop("remove_view_groups", None)
+                remove_change_users = action.pop("remove_change_users", None)
+                remove_change_groups = action.pop("remove_change_groups", None)
+
                 action_instance, _ = WorkflowAction.objects.update_or_create(
                     id=action.get("id"),
                     defaults=action,
                 )
+
                 if assign_tags is not None:
                     action_instance.assign_tags.set(assign_tags)
                 if assign_view_users is not None:
@@ -1567,6 +1596,27 @@ class WorkflowSerializer(serializers.ModelSerializer):
                     action_instance.assign_change_groups.set(assign_change_groups)
                 if assign_custom_fields is not None:
                     action_instance.assign_custom_fields.set(assign_custom_fields)
+                if remove_tags is not None:
+                    action_instance.remove_tags.set(remove_tags)
+                if remove_correspondents is not None:
+                    action_instance.remove_correspondents.set(remove_correspondents)
+                if remove_document_types is not None:
+                    action_instance.remove_document_types.set(remove_document_types)
+                if remove_storage_paths is not None:
+                    action_instance.remove_storage_paths.set(remove_storage_paths)
+                if remove_custom_fields is not None:
+                    action_instance.remove_custom_fields.set(remove_custom_fields)
+                if remove_owners is not None:
+                    action_instance.remove_owners.set(remove_owners)
+                if remove_view_users is not None:
+                    action_instance.remove_view_users.set(remove_view_users)
+                if remove_view_groups is not None:
+                    action_instance.remove_view_groups.set(remove_view_groups)
+                if remove_change_users is not None:
+                    action_instance.remove_change_users.set(remove_change_users)
+                if remove_change_groups is not None:
+                    action_instance.remove_change_groups.set(remove_change_groups)
+
                 set_actions.append(action_instance)
 
         instance.triggers.set(set_triggers)
index b6903d98cf23684f6b1995eea444895e0ea5c23c..85e8126c1ff63e7c07ca49d83ee6143fbfcd426e 100644 (file)
@@ -20,6 +20,7 @@ from django.db.models import Q
 from django.dispatch import receiver
 from django.utils import timezone
 from filelock import FileLock
+from guardian.shortcuts import remove_perm
 
 from documents import matching
 from documents.caching import clear_metadata_cache
@@ -34,6 +35,7 @@ from documents.models import MatchingModel
 from documents.models import PaperlessTask
 from documents.models import Tag
 from documents.models import Workflow
+from documents.models import WorkflowAction
 from documents.models import WorkflowTrigger
 from documents.permissions import get_objects_for_user_owner_aware
 from documents.permissions import set_permissions_for_object
@@ -529,123 +531,232 @@ def run_workflow(
     document: Document,
     logging_group=None,
 ):
-    for workflow in Workflow.objects.filter(
-        enabled=True,
-        triggers__type=trigger_type,
-    ).order_by("order"):
+    for workflow in (
+        Workflow.objects.filter(
+            enabled=True,
+            triggers__type=trigger_type,
+        )
+        .prefetch_related("actions")
+        .prefetch_related("actions__assign_view_users")
+        .prefetch_related("actions__assign_view_groups")
+        .prefetch_related("actions__assign_change_users")
+        .prefetch_related("actions__assign_change_groups")
+        .prefetch_related("actions__assign_custom_fields")
+        .prefetch_related("actions__remove_tags")
+        .prefetch_related("actions__remove_correspondents")
+        .prefetch_related("actions__remove_document_types")
+        .prefetch_related("actions__remove_storage_paths")
+        .prefetch_related("actions__remove_custom_fields")
+        .prefetch_related("actions__remove_owners")
+        .prefetch_related("triggers")
+        .order_by("order")
+    ):
         if matching.document_matches_workflow(
             document,
             workflow,
             trigger_type,
         ):
+            action: WorkflowAction
             for action in workflow.actions.all():
                 logger.info(
                     f"Applying {action} from {workflow}",
                     extra={"group": logging_group},
                 )
-                if action.assign_tags.all().count() > 0:
-                    document.tags.add(*action.assign_tags.all())
-
-                if action.assign_correspondent is not None:
-                    document.correspondent = action.assign_correspondent
-
-                if action.assign_document_type is not None:
-                    document.document_type = action.assign_document_type
-
-                if action.assign_storage_path is not None:
-                    document.storage_path = action.assign_storage_path
-
-                if action.assign_owner is not None:
-                    document.owner = action.assign_owner
-
-                if action.assign_title is not None:
-                    try:
-                        document.title = parse_doc_title_w_placeholders(
-                            action.assign_title,
-                            (
-                                document.correspondent.name
-                                if document.correspondent is not None
-                                else ""
-                            ),
-                            (
-                                document.document_type.name
-                                if document.document_type is not None
-                                else ""
-                            ),
-                            (
-                                document.owner.username
-                                if document.owner is not None
-                                else ""
-                            ),
-                            timezone.localtime(document.added),
-                            (
-                                document.original_filename
-                                if document.original_filename is not None
-                                else ""
-                            ),
-                            timezone.localtime(document.created),
-                        )
-                    except Exception:
-                        logger.exception(
-                            f"Error occurred parsing title assignment '{action.assign_title}', falling back to original",
-                            extra={"group": logging_group},
-                        )
 
-                if (
-                    (
-                        action.assign_view_users is not None
-                        and action.assign_view_users.count() > 0
-                    )
-                    or (
-                        action.assign_view_groups is not None
-                        and action.assign_view_groups.count() > 0
-                    )
-                    or (
-                        action.assign_change_users is not None
-                        and action.assign_change_users.count() > 0
-                    )
-                    or (
-                        action.assign_change_groups is not None
-                        and action.assign_change_groups.count() > 0
-                    )
-                ):
-                    permissions = {
-                        "view": {
-                            "users": action.assign_view_users.all().values_list("id")
-                            or [],
-                            "groups": action.assign_view_groups.all().values_list("id")
-                            or [],
-                        },
-                        "change": {
-                            "users": action.assign_change_users.all().values_list("id")
-                            or [],
-                            "groups": action.assign_change_groups.all().values_list(
-                                "id",
+                if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT:
+                    if action.assign_tags.all().count() > 0:
+                        document.tags.add(*action.assign_tags.all())
+
+                    if action.assign_correspondent is not None:
+                        document.correspondent = action.assign_correspondent
+
+                    if action.assign_document_type is not None:
+                        document.document_type = action.assign_document_type
+
+                    if action.assign_storage_path is not None:
+                        document.storage_path = action.assign_storage_path
+
+                    if action.assign_owner is not None:
+                        document.owner = action.assign_owner
+
+                    if action.assign_title is not None:
+                        try:
+                            document.title = parse_doc_title_w_placeholders(
+                                action.assign_title,
+                                (
+                                    document.correspondent.name
+                                    if document.correspondent is not None
+                                    else ""
+                                ),
+                                (
+                                    document.document_type.name
+                                    if document.document_type is not None
+                                    else ""
+                                ),
+                                (
+                                    document.owner.username
+                                    if document.owner is not None
+                                    else ""
+                                ),
+                                timezone.localtime(document.added),
+                                (
+                                    document.original_filename
+                                    if document.original_filename is not None
+                                    else ""
+                                ),
+                                timezone.localtime(document.created),
                             )
-                            or [],
-                        },
-                    }
-                    set_permissions_for_object(
-                        permissions=permissions,
-                        object=document,
-                        merge=True,
-                    )
-
-                if action.assign_custom_fields is not None:
-                    for field in action.assign_custom_fields.all():
-                        if (
-                            CustomFieldInstance.objects.filter(
-                                field=field,
-                                document=document,
-                            ).count()
-                            == 0
-                        ):
-                            # can be triggered on existing docs, so only add the field if it doesn't already exist
-                            CustomFieldInstance.objects.create(
-                                field=field,
-                                document=document,
+                        except Exception:
+                            logger.exception(
+                                f"Error occurred parsing title assignment '{action.assign_title}', falling back to original",
+                                extra={"group": logging_group},
                             )
 
+                    if (
+                        (
+                            action.assign_view_users is not None
+                            and action.assign_view_users.count() > 0
+                        )
+                        or (
+                            action.assign_view_groups is not None
+                            and action.assign_view_groups.count() > 0
+                        )
+                        or (
+                            action.assign_change_users is not None
+                            and action.assign_change_users.count() > 0
+                        )
+                        or (
+                            action.assign_change_groups is not None
+                            and action.assign_change_groups.count() > 0
+                        )
+                    ):
+                        permissions = {
+                            "view": {
+                                "users": action.assign_view_users.all().values_list(
+                                    "id",
+                                )
+                                or [],
+                                "groups": action.assign_view_groups.all().values_list(
+                                    "id",
+                                )
+                                or [],
+                            },
+                            "change": {
+                                "users": action.assign_change_users.all().values_list(
+                                    "id",
+                                )
+                                or [],
+                                "groups": action.assign_change_groups.all().values_list(
+                                    "id",
+                                )
+                                or [],
+                            },
+                        }
+                        set_permissions_for_object(
+                            permissions=permissions,
+                            object=document,
+                            merge=True,
+                        )
+
+                    if action.assign_custom_fields is not None:
+                        for field in action.assign_custom_fields.all():
+                            if (
+                                CustomFieldInstance.objects.filter(
+                                    field=field,
+                                    document=document,
+                                ).count()
+                                == 0
+                            ):
+                                # can be triggered on existing docs, so only add the field if it doesn't already exist
+                                CustomFieldInstance.objects.create(
+                                    field=field,
+                                    document=document,
+                                )
+
+                elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
+                    if action.remove_all_tags:
+                        document.tags.clear()
+                    else:
+                        for tag in action.remove_tags.filter(
+                            pk__in=list(document.tags.values_list("pk", flat=True)),
+                        ).all():
+                            document.tags.remove(tag.pk)
+
+                    if action.remove_all_correspondents or (
+                        document.correspondent
+                        and (
+                            action.remove_correspondents.filter(
+                                pk=document.correspondent.pk,
+                            ).exists()
+                        )
+                    ):
+                        document.correspondent = None
+
+                    if action.remove_all_document_types or (
+                        document.document_type
+                        and (
+                            action.remove_document_types.filter(
+                                pk=document.document_type.pk,
+                            ).exists()
+                        )
+                    ):
+                        document.document_type = None
+
+                    if action.remove_all_storage_paths or (
+                        document.storage_path
+                        and (
+                            action.remove_storage_paths.filter(
+                                pk=document.storage_path.pk,
+                            ).exists()
+                        )
+                    ):
+                        document.storage_path = None
+
+                    if action.remove_all_owners or (
+                        document.owner
+                        and (action.remove_owners.filter(pk=document.owner.pk).exists())
+                    ):
+                        document.owner = None
+
+                    if action.remove_all_permissions:
+                        permissions = {
+                            "view": {
+                                "users": [],
+                                "groups": [],
+                            },
+                            "change": {
+                                "users": [],
+                                "groups": [],
+                            },
+                        }
+                        set_permissions_for_object(
+                            permissions=permissions,
+                            object=document,
+                            merge=False,
+                        )
+                    elif (
+                        (action.remove_view_users.all().count() > 0)
+                        or (action.remove_view_groups.all().count() > 0)
+                        or (action.remove_change_users.all().count() > 0)
+                        or (action.remove_change_groups.all().count() > 0)
+                    ):
+                        for user in action.remove_view_users.all():
+                            remove_perm("view_document", user, document)
+                        for user in action.remove_change_users.all():
+                            remove_perm("change_document", user, document)
+                        for group in action.remove_view_groups.all():
+                            remove_perm("view_document", group, document)
+                        for group in action.remove_change_groups.all():
+                            remove_perm("change_document", group, document)
+
+                    if action.remove_all_custom_fields:
+                        CustomFieldInstance.objects.filter(document=document).delete()
+                    elif action.remove_custom_fields.all().count() > 0:
+                        CustomFieldInstance.objects.filter(
+                            field__in=action.remove_custom_fields.all(),
+                            document=document,
+                        ).delete()
+
             document.save()
 
 
index 0751d0df559500abe14a45ad27f5a728d8bf21ce..7f48347c070495229d86cbbc015b221a6206fcd6 100644 (file)
@@ -202,6 +202,19 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
                             "assign_change_groups": [self.group1.id],
                             "assign_custom_fields": [self.cf2.id],
                         },
+                        {
+                            "type": WorkflowAction.WorkflowActionType.REMOVAL,
+                            "remove_tags": [self.t3.id],
+                            "remove_document_types": [self.dt.id],
+                            "remove_correspondents": [self.c.id],
+                            "remove_storage_paths": [self.sp.id],
+                            "remove_custom_fields": [self.cf1.id],
+                            "remove_owners": [self.user2.id],
+                            "remove_view_users": [self.user3.id],
+                            "remove_change_users": [self.user3.id],
+                            "remove_view_groups": [self.group1.id],
+                            "remove_change_groups": [self.group1.id],
+                        },
                     ],
                 },
             ),
index 95f9032393ed45c1a1d9e9486c37bb6949684387..509a8e54dca7cd0f26aaf5988667afe05c5e14de 100644 (file)
@@ -1223,3 +1223,332 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
             title="test",
         )
         self.assertRaises(Exception, document_matches_workflow, doc, w, 4)
+
+    def test_removal_action_document_updated_workflow(self):
+        """
+        GIVEN:
+            - Workflow with removal action
+        WHEN:
+            - File that matches is updated
+        THEN:
+            - Action removals are applied
+        """
+        trigger = WorkflowTrigger.objects.create(
+            type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+            filter_path="*",
+        )
+        action = WorkflowAction.objects.create(
+            type=WorkflowAction.WorkflowActionType.REMOVAL,
+        )
+        action.remove_correspondents.add(self.c)
+        action.remove_tags.add(self.t1)
+        action.remove_document_types.add(self.dt)
+        action.remove_storage_paths.add(self.sp)
+        action.remove_owners.add(self.user2)
+        action.remove_custom_fields.add(self.cf1)
+        action.remove_view_users.add(self.user3)
+        action.remove_view_groups.add(self.group1)
+        action.remove_change_users.add(self.user3)
+        action.remove_change_groups.add(self.group1)
+        action.save()
+
+        w = Workflow.objects.create(
+            name="Workflow 1",
+            order=0,
+        )
+        w.triggers.add(trigger)
+        w.actions.add(action)
+        w.save()
+
+        doc = Document.objects.create(
+            title="sample test",
+            correspondent=self.c,
+            document_type=self.dt,
+            storage_path=self.sp,
+            owner=self.user2,
+            original_filename="sample.pdf",
+        )
+        doc.tags.set([self.t1, self.t2])
+        CustomFieldInstance.objects.create(document=doc, field=self.cf1)
+        doc.save()
+        assign_perm("documents.view_document", self.user3, doc)
+        assign_perm("documents.change_document", self.user3, doc)
+        assign_perm("documents.view_document", self.group1, doc)
+        assign_perm("documents.change_document", self.group1, doc)
+
+        superuser = User.objects.create_superuser("superuser")
+        self.client.force_authenticate(user=superuser)
+
+        self.client.patch(
+            f"/api/documents/{doc.id}/",
+            {"title": "new title"},
+            format="json",
+        )
+        doc.refresh_from_db()
+
+        self.assertIsNone(doc.document_type)
+        self.assertIsNone(doc.correspondent)
+        self.assertIsNone(doc.storage_path)
+        self.assertEqual(doc.tags.all().count(), 1)
+        self.assertIn(self.t2, doc.tags.all())
+        self.assertIsNone(doc.owner)
+        self.assertEqual(doc.custom_fields.all().count(), 0)
+        self.assertFalse(self.user3.has_perm("documents.view_document", doc))
+        self.assertFalse(self.user3.has_perm("documents.change_document", doc))
+        group_perms: QuerySet = get_groups_with_perms(doc)
+        self.assertNotIn(self.group1, group_perms)
+
+    def test_removal_action_document_updated_removeall(self):
+        """
+        GIVEN:
+            - Workflow with removal action with remove all fields set
+        WHEN:
+            - File that matches is updated
+        THEN:
+            - Action removals are applied
+        """
+        trigger = WorkflowTrigger.objects.create(
+            type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+            filter_path="*",
+        )
+        action = WorkflowAction.objects.create(
+            type=WorkflowAction.WorkflowActionType.REMOVAL,
+            remove_all_correspondents=True,
+            remove_all_tags=True,
+            remove_all_document_types=True,
+            remove_all_storage_paths=True,
+            remove_all_custom_fields=True,
+            remove_all_owners=True,
+            remove_all_permissions=True,
+        )
+        action.save()
+
+        w = Workflow.objects.create(
+            name="Workflow 1",
+            order=0,
+        )
+        w.triggers.add(trigger)
+        w.actions.add(action)
+        w.save()
+
+        doc = Document.objects.create(
+            title="sample test",
+            correspondent=self.c,
+            document_type=self.dt,
+            storage_path=self.sp,
+            owner=self.user2,
+            original_filename="sample.pdf",
+        )
+        doc.tags.set([self.t1, self.t2])
+        CustomFieldInstance.objects.create(document=doc, field=self.cf1)
+        doc.save()
+        assign_perm("documents.view_document", self.user3, doc)
+        assign_perm("documents.change_document", self.user3, doc)
+        assign_perm("documents.view_document", self.group1, doc)
+        assign_perm("documents.change_document", self.group1, doc)
+
+        superuser = User.objects.create_superuser("superuser")
+        self.client.force_authenticate(user=superuser)
+
+        self.client.patch(
+            f"/api/documents/{doc.id}/",
+            {"title": "new title"},
+            format="json",
+        )
+        doc.refresh_from_db()
+
+        self.assertIsNone(doc.document_type)
+        self.assertIsNone(doc.correspondent)
+        self.assertIsNone(doc.storage_path)
+        self.assertEqual(doc.tags.all().count(), 0)
+        self.assertEqual(doc.tags.all().count(), 0)
+        self.assertIsNone(doc.owner)
+        self.assertEqual(doc.custom_fields.all().count(), 0)
+        self.assertFalse(self.user3.has_perm("documents.view_document", doc))
+        self.assertFalse(self.user3.has_perm("documents.change_document", doc))
+        group_perms: QuerySet = get_groups_with_perms(doc)
+        self.assertNotIn(self.group1, group_perms)
+
+    @mock.patch("documents.consumer.Consumer.try_consume_file")
+    def test_removal_action_document_consumed(self, m):
+        """
+        GIVEN:
+            - Workflow with assignment and removal actions
+        WHEN:
+            - File that matches is consumed
+        THEN:
+            - Action removals are applied
+        """
+        trigger = WorkflowTrigger.objects.create(
+            type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+            filter_filename="*simple*",
+        )
+        action = WorkflowAction.objects.create(
+            assign_title="Doc from {correspondent}",
+            assign_correspondent=self.c,
+            assign_document_type=self.dt,
+            assign_storage_path=self.sp,
+            assign_owner=self.user2,
+        )
+        action.assign_tags.add(self.t1)
+        action.assign_tags.add(self.t2)
+        action.assign_tags.add(self.t3)
+        action.assign_view_users.add(self.user2)
+        action.assign_view_users.add(self.user3)
+        action.assign_view_groups.add(self.group1)
+        action.assign_view_groups.add(self.group2)
+        action.assign_change_users.add(self.user2)
+        action.assign_change_users.add(self.user3)
+        action.assign_change_groups.add(self.group1)
+        action.assign_change_groups.add(self.group2)
+        action.assign_custom_fields.add(self.cf1)
+        action.assign_custom_fields.add(self.cf2)
+        action.save()
+
+        action2 = WorkflowAction.objects.create(
+            type=WorkflowAction.WorkflowActionType.REMOVAL,
+        )
+        action2.remove_correspondents.add(self.c)
+        action2.remove_tags.add(self.t1)
+        action2.remove_document_types.add(self.dt)
+        action2.remove_storage_paths.add(self.sp)
+        action2.remove_owners.add(self.user2)
+        action2.remove_custom_fields.add(self.cf1)
+        action2.remove_view_users.add(self.user3)
+        action2.remove_change_users.add(self.user3)
+        action2.remove_view_groups.add(self.group1)
+        action2.remove_change_groups.add(self.group1)
+        action2.save()
+
+        w = Workflow.objects.create(
+            name="Workflow 1",
+            order=0,
+        )
+        w.triggers.add(trigger)
+        w.actions.add(action)
+        w.actions.add(action2)
+        w.save()
+
+        test_file = self.SAMPLE_DIR / "simple.pdf"
+
+        with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
+            with self.assertLogs("paperless.matching", level="INFO") as cm:
+                tasks.consume_file(
+                    ConsumableDocument(
+                        source=DocumentSource.ConsumeFolder,
+                        original_file=test_file,
+                    ),
+                    None,
+                )
+                m.assert_called_once()
+                _, overrides = m.call_args
+                self.assertIsNone(overrides["override_correspondent_id"])
+                self.assertIsNone(overrides["override_document_type_id"])
+                self.assertEqual(
+                    overrides["override_tag_ids"],
+                    [self.t2.pk, self.t3.pk],
+                )
+                self.assertIsNone(overrides["override_storage_path_id"])
+                self.assertIsNone(overrides["override_owner_id"])
+                self.assertEqual(overrides["override_view_users"], [self.user2.pk])
+                self.assertEqual(overrides["override_view_groups"], [self.group2.pk])
+                self.assertEqual(overrides["override_change_users"], [self.user2.pk])
+                self.assertEqual(overrides["override_change_groups"], [self.group2.pk])
+                self.assertEqual(
+                    overrides["override_title"],
+                    "Doc from {correspondent}",
+                )
+                self.assertEqual(
+                    overrides["override_custom_field_ids"],
+                    [self.cf2.pk],
+                )
+
+        info = cm.output[0]
+        expected_str = f"Document matched {trigger} from {w}"
+        self.assertIn(expected_str, info)
+
+    @mock.patch("documents.consumer.Consumer.try_consume_file")
+    def test_removal_action_document_consumed_removeall(self, m):
+        """
+        GIVEN:
+            - Workflow with assignment and removal actions with remove all fields set
+        WHEN:
+            - File that matches is consumed
+        THEN:
+            - Action removals are applied
+        """
+        trigger = WorkflowTrigger.objects.create(
+            type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+            filter_filename="*simple*",
+        )
+        action = WorkflowAction.objects.create(
+            assign_title="Doc from {correspondent}",
+            assign_correspondent=self.c,
+            assign_document_type=self.dt,
+            assign_storage_path=self.sp,
+            assign_owner=self.user2,
+        )
+        action.assign_tags.add(self.t1)
+        action.assign_tags.add(self.t2)
+        action.assign_tags.add(self.t3)
+        action.assign_view_users.add(self.user3.pk)
+        action.assign_view_groups.add(self.group1.pk)
+        action.assign_change_users.add(self.user3.pk)
+        action.assign_change_groups.add(self.group1.pk)
+        action.assign_custom_fields.add(self.cf1.pk)
+        action.assign_custom_fields.add(self.cf2.pk)
+        action.save()
+
+        action2 = WorkflowAction.objects.create(
+            type=WorkflowAction.WorkflowActionType.REMOVAL,
+            remove_all_correspondents=True,
+            remove_all_tags=True,
+            remove_all_document_types=True,
+            remove_all_storage_paths=True,
+            remove_all_custom_fields=True,
+            remove_all_owners=True,
+            remove_all_permissions=True,
+        )
+
+        w = Workflow.objects.create(
+            name="Workflow 1",
+            order=0,
+        )
+        w.triggers.add(trigger)
+        w.actions.add(action)
+        w.actions.add(action2)
+        w.save()
+
+        test_file = self.SAMPLE_DIR / "simple.pdf"
+
+        with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
+            with self.assertLogs("paperless.matching", level="INFO") as cm:
+                tasks.consume_file(
+                    ConsumableDocument(
+                        source=DocumentSource.ConsumeFolder,
+                        original_file=test_file,
+                    ),
+                    None,
+                )
+                m.assert_called_once()
+                _, overrides = m.call_args
+                self.assertIsNone(overrides["override_correspondent_id"])
+                self.assertIsNone(overrides["override_document_type_id"])
+                self.assertEqual(
+                    overrides["override_tag_ids"],
+                    [],
+                )
+                self.assertIsNone(overrides["override_storage_path_id"])
+                self.assertIsNone(overrides["override_owner_id"])
+                self.assertEqual(overrides["override_view_users"], [])
+                self.assertEqual(overrides["override_view_groups"], [])
+                self.assertEqual(overrides["override_change_users"], [])
+                self.assertEqual(overrides["override_change_groups"], [])
+                self.assertEqual(
+                    overrides["override_custom_field_ids"],
+                    [],
+                )
+
+        info = cm.output[0]
+        expected_str = f"Document matched {trigger} from {w}"
+        self.assertIn(expected_str, info)
index 0689b523ca3093fd2bdf88340a292767b7d23b46..4d56bdeb3a1e2fcb26d57d767a7e67460037685b 100644 (file)
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: paperless-ngx\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-26 13:34-0800\n"
+"POT-Creation-Date: 2024-02-27 10:51-0800\n"
 "PO-Revision-Date: 2022-02-17 04:17\n"
 "Last-Translator: \n"
 "Language-Team: English\n"
@@ -53,7 +53,7 @@ msgstr ""
 msgid "Automatic"
 msgstr ""
 
-#: documents/models.py:62 documents/models.py:397 documents/models.py:1102
+#: documents/models.py:62 documents/models.py:397 documents/models.py:1218
 #: paperless_mail/models.py:18 paperless_mail/models.py:93
 msgid "name"
 msgstr ""
@@ -687,102 +687,174 @@ msgstr ""
 msgid "workflow triggers"
 msgstr ""
 
-#: documents/models.py:1000
+#: documents/models.py:1002
 msgid "Assignment"
 msgstr ""
 
-#: documents/models.py:1003
+#: documents/models.py:1006
+msgid "Removal"
+msgstr ""
+
+#: documents/models.py:1010
 msgid "Workflow Action Type"
 msgstr ""
 
-#: documents/models.py:1009
+#: documents/models.py:1016
 msgid "assign title"
 msgstr ""
 
-#: documents/models.py:1014
+#: documents/models.py:1021
 msgid ""
 "Assign a document title, can include some placeholders, see documentation."
 msgstr ""
 
-#: documents/models.py:1022 paperless_mail/models.py:216
+#: documents/models.py:1030 paperless_mail/models.py:216
 msgid "assign this tag"
 msgstr ""
 
-#: documents/models.py:1030 paperless_mail/models.py:224
+#: documents/models.py:1039 paperless_mail/models.py:224
 msgid "assign this document type"
 msgstr ""
 
-#: documents/models.py:1038 paperless_mail/models.py:238
+#: documents/models.py:1048 paperless_mail/models.py:238
 msgid "assign this correspondent"
 msgstr ""
 
-#: documents/models.py:1046
+#: documents/models.py:1057
 msgid "assign this storage path"
 msgstr ""
 
-#: documents/models.py:1055
+#: documents/models.py:1066
 msgid "assign this owner"
 msgstr ""
 
-#: documents/models.py:1062
+#: documents/models.py:1073
 msgid "grant view permissions to these users"
 msgstr ""
 
-#: documents/models.py:1069
+#: documents/models.py:1080
 msgid "grant view permissions to these groups"
 msgstr ""
 
-#: documents/models.py:1076
+#: documents/models.py:1087
 msgid "grant change permissions to these users"
 msgstr ""
 
-#: documents/models.py:1083
+#: documents/models.py:1094
 msgid "grant change permissions to these groups"
 msgstr ""
 
-#: documents/models.py:1090
+#: documents/models.py:1101
 msgid "assign these custom fields"
 msgstr ""
 
-#: documents/models.py:1094
+#: documents/models.py:1108
+msgid "remove these tag(s)"
+msgstr ""
+
+#: documents/models.py:1113
+msgid "remove all tags"
+msgstr ""
+
+#: documents/models.py:1120
+msgid "remove these document type(s)"
+msgstr ""
+
+#: documents/models.py:1125
+msgid "remove all document types"
+msgstr ""
+
+#: documents/models.py:1132
+msgid "remove these correspondent(s)"
+msgstr ""
+
+#: documents/models.py:1137
+msgid "remove all correspondents"
+msgstr ""
+
+#: documents/models.py:1144
+msgid "remove these storage path(s)"
+msgstr ""
+
+#: documents/models.py:1149
+msgid "remove all storage paths"
+msgstr ""
+
+#: documents/models.py:1156
+msgid "remove these owner(s)"
+msgstr ""
+
+#: documents/models.py:1161
+msgid "remove all owners"
+msgstr ""
+
+#: documents/models.py:1168
+msgid "remove view permissions for these users"
+msgstr ""
+
+#: documents/models.py:1175
+msgid "remove view permissions for these groups"
+msgstr ""
+
+#: documents/models.py:1182
+msgid "remove change permissions for these users"
+msgstr ""
+
+#: documents/models.py:1189
+msgid "remove change permissions for these groups"
+msgstr ""
+
+#: documents/models.py:1194
+msgid "remove all permissions"
+msgstr ""
+
+#: documents/models.py:1201
+msgid "remove these custom fields"
+msgstr ""
+
+#: documents/models.py:1206
+msgid "remove all custom fields"
+msgstr ""
+
+#: documents/models.py:1210
 msgid "workflow action"
 msgstr ""
 
-#: documents/models.py:1095
+#: documents/models.py:1211
 msgid "workflow actions"
 msgstr ""
 
-#: documents/models.py:1104 paperless_mail/models.py:95
+#: documents/models.py:1220 paperless_mail/models.py:95
 msgid "order"
 msgstr ""
 
-#: documents/models.py:1110
+#: documents/models.py:1226
 msgid "triggers"
 msgstr ""
 
-#: documents/models.py:1117
+#: documents/models.py:1233
 msgid "actions"
 msgstr ""
 
-#: documents/models.py:1120
+#: documents/models.py:1236
 msgid "enabled"
 msgstr ""
 
-#: documents/serialisers.py:113
+#: documents/serialisers.py:114
 #, python-format
 msgid "Invalid regular expression: %(error)s"
 msgstr ""
 
-#: documents/serialisers.py:407
+#: documents/serialisers.py:408
 msgid "Invalid color."
 msgstr ""
 
-#: documents/serialisers.py:1073
+#: documents/serialisers.py:1070
 #, python-format
 msgid "File type %(type)s not supported"
 msgstr ""
 
-#: documents/serialisers.py:1176
+#: documents/serialisers.py:1173
 msgid "Invalid variable detected."
 msgstr ""