mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: shared icon & shared by me filter (#4859)
This commit is contained in:
		| @@ -80,7 +80,7 @@ django_checks() { | ||||
|  | ||||
| search_index() { | ||||
|  | ||||
| 	local -r index_version=7 | ||||
| 	local -r index_version=8 | ||||
| 	local -r index_version_file=${DATA_DIR}/.index_version | ||||
|  | ||||
| 	if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then | ||||
|   | ||||
| @@ -1657,7 +1657,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">68</context> | ||||
|           <context context-type="linenumber">78</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2941198503117307737" datatype="html"> | ||||
| @@ -1721,7 +1721,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> | ||||
|           <context context-type="linenumber">83</context> | ||||
|           <context context-type="linenumber">89</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/consumption-templates/consumption-templates.component.html</context> | ||||
| @@ -3670,18 +3670,25 @@ | ||||
|           <context context-type="linenumber">38</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="175385209536581523" datatype="html"> | ||||
|         <source>Shared by me</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">48</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5151074932731293042" datatype="html"> | ||||
|         <source>Unowned</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">48</context> | ||||
|           <context context-type="linenumber">58</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8999708063434507268" datatype="html"> | ||||
|         <source>Hide unowned</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">77</context> | ||||
|           <context context-type="linenumber">87</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8650499415827640724" datatype="html"> | ||||
| @@ -4012,7 +4019,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">203</context> | ||||
|           <context context-type="linenumber">204</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||
| @@ -4073,7 +4080,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> | ||||
|           <context context-type="linenumber">99</context> | ||||
|           <context context-type="linenumber">105</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="872092479747931526" datatype="html"> | ||||
| @@ -5007,11 +5014,26 @@ | ||||
|           <context context-type="linenumber">58,59</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5739581984228459958" datatype="html"> | ||||
|         <source>Shared</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> | ||||
|           <context context-type="linenumber">119</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> | ||||
|           <context context-type="linenumber">84</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/pipes/username.pipe.ts</context> | ||||
|           <context context-type="linenumber">33</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2332107018974972998" datatype="html"> | ||||
|         <source>Score:</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> | ||||
|           <context context-type="linenumber">116</context> | ||||
|           <context context-type="linenumber">122</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3661756380991326939" datatype="html"> | ||||
| @@ -5138,7 +5160,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">208</context> | ||||
|           <context context-type="linenumber">209</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||
| @@ -5254,14 +5276,14 @@ | ||||
|             )?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">120,122</context> | ||||
|           <context context-type="linenumber">121,123</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8170755470576301659" datatype="html"> | ||||
|         <source>Without correspondent</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">124</context> | ||||
|           <context context-type="linenumber">125</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="317796810569008208" datatype="html"> | ||||
| @@ -5270,14 +5292,14 @@ | ||||
|             )?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">130,132</context> | ||||
|           <context context-type="linenumber">131,133</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4362173610367509215" datatype="html"> | ||||
|         <source>Without document type</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">134</context> | ||||
|           <context context-type="linenumber">135</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="232202047340644471" datatype="html"> | ||||
| @@ -5286,14 +5308,14 @@ | ||||
|             )?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">140,142</context> | ||||
|           <context context-type="linenumber">141,143</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1562820715074533164" datatype="html"> | ||||
|         <source>Without storage path</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">144</context> | ||||
|           <context context-type="linenumber">145</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8180755793012580465" datatype="html"> | ||||
| @@ -5301,112 +5323,112 @@ | ||||
|             ?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">148,149</context> | ||||
|           <context context-type="linenumber">149,150</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6494566478302448576" datatype="html"> | ||||
|         <source>Without any tag</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">153</context> | ||||
|           <context context-type="linenumber">154</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6523384805359286307" datatype="html"> | ||||
|         <source>Title: <x id="PH" equiv-text="rule.value"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">157</context> | ||||
|           <context context-type="linenumber">158</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1872523635812236432" datatype="html"> | ||||
|         <source>ASN: <x id="PH" equiv-text="rule.value"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">160</context> | ||||
|           <context context-type="linenumber">161</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="102674688969746976" datatype="html"> | ||||
|         <source>Owner: <x id="PH" equiv-text="rule.value"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">163</context> | ||||
|           <context context-type="linenumber">164</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3550877650686009106" datatype="html"> | ||||
|         <source>Owner not in: <x id="PH" equiv-text="rule.value"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">166</context> | ||||
|           <context context-type="linenumber">167</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1082034558646673343" datatype="html"> | ||||
|         <source>Without an owner</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">169</context> | ||||
|           <context context-type="linenumber">170</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3100631071441658964" datatype="html"> | ||||
|         <source>Title & content</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">206</context> | ||||
|           <context context-type="linenumber">207</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="9149498548977462220" datatype="html"> | ||||
|         <source>Custom fields</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">211</context> | ||||
|           <context context-type="linenumber">212</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1010505078885609376" datatype="html"> | ||||
|         <source>Advanced search</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">215</context> | ||||
|           <context context-type="linenumber">216</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2649431021108393503" datatype="html"> | ||||
|         <source>More like</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">221</context> | ||||
|           <context context-type="linenumber">222</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3697582909018473071" datatype="html"> | ||||
|         <source>equals</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">240</context> | ||||
|           <context context-type="linenumber">241</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5325481293405718739" datatype="html"> | ||||
|         <source>is empty</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">244</context> | ||||
|           <context context-type="linenumber">245</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6166785695326182482" datatype="html"> | ||||
|         <source>is not empty</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">248</context> | ||||
|           <context context-type="linenumber">249</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4686622206659266699" datatype="html"> | ||||
|         <source>greater than</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">252</context> | ||||
|           <context context-type="linenumber">253</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8014012170270529279" datatype="html"> | ||||
|         <source>less than</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">256</context> | ||||
|           <context context-type="linenumber">257</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7210076240260527720" datatype="html"> | ||||
| @@ -6297,13 +6319,6 @@ | ||||
|           <context context-type="linenumber">11</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5739581984228459958" datatype="html"> | ||||
|         <source>Shared</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/pipes/username.pipe.ts</context> | ||||
|           <context context-type="linenumber">33</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2807800733729323332" datatype="html"> | ||||
|         <source>Yes</source> | ||||
|         <context-group purpose="location"> | ||||
|   | ||||
| @@ -38,6 +38,16 @@ | ||||
|                     <small i18n>Shared with me</small> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME" fill="currentColor" class="buttonicon-sm"> | ||||
|                         <use xlink:href="assets/bootstrap-icons.svg#check"/> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|                 <div class="me-1"> | ||||
|                     <small i18n>Shared by me</small> | ||||
|                 </div> | ||||
|             </button> | ||||
|             <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled"> | ||||
|                 <div class="selected-icon me-1"> | ||||
|                     <svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" fill="currentColor" class="buttonicon-sm"> | ||||
|   | ||||
| @@ -145,6 +145,15 @@ describe('PermissionsFilterDropdownComponent', () => { | ||||
|       userID: null, | ||||
|     }) | ||||
|  | ||||
|     component.setFilter(OwnerFilterType.SHARED_BY_ME) | ||||
|     expect(ownerFilterSetResult).toEqual({ | ||||
|       excludeUsers: [], | ||||
|       hideUnowned: false, | ||||
|       includeUsers: [], | ||||
|       ownerFilter: OwnerFilterType.SHARED_BY_ME, | ||||
|       userID: currentUserID, | ||||
|     }) | ||||
|  | ||||
|     component.setFilter(OwnerFilterType.UNOWNED) | ||||
|     expect(ownerFilterSetResult).toEqual({ | ||||
|       excludeUsers: [], | ||||
|   | ||||
| @@ -32,6 +32,7 @@ export enum OwnerFilterType { | ||||
|   NOT_SELF = 2, | ||||
|   OTHERS = 3, | ||||
|   UNOWNED = 4, | ||||
|   SHARED_BY_ME = 5, | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
| @@ -108,6 +109,13 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions | ||||
|       this.selectionModel.includeUsers = [] | ||||
|       this.selectionModel.excludeUsers = [] | ||||
|       this.selectionModel.hideUnowned = false | ||||
|     } else if ( | ||||
|       this.selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME | ||||
|     ) { | ||||
|       this.selectionModel.userID = this.settingsService.currentUser.id | ||||
|       this.selectionModel.includeUsers = [] | ||||
|       this.selectionModel.excludeUsers = [] | ||||
|       this.selectionModel.hideUnowned = false | ||||
|     } else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) { | ||||
|       this.selectionModel.userID = null | ||||
|       this.selectionModel.includeUsers = [] | ||||
|   | ||||
| @@ -112,6 +112,12 @@ | ||||
|               </svg> | ||||
|               <small>{{document.owner | username}}</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.is_shared_by_requester" class="list-group-item bg-light text-dark p-1 border-0"> | ||||
|               <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#people-fill"/> | ||||
|               </svg> | ||||
|               <small i18n>Shared</small> | ||||
|             </div> | ||||
|             <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> | ||||
|               <small class="text-muted" i18n>Score:</small> | ||||
|               <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|   | ||||
| @@ -77,6 +77,12 @@ | ||||
|           </svg> | ||||
|           <small>{{document.owner | username}}</small> | ||||
|         </div> | ||||
|         <div *ngIf="document.is_shared_by_requester" class="ps-0 p-1"> | ||||
|           <svg class="metadata-icon me-2 text-muted" fill="currentColor"> | ||||
|             <use xlink:href="assets/bootstrap-icons.svg#people-fill"/> | ||||
|           </svg> | ||||
|           <small i18n>Shared</small> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="d-flex justify-content-between align-items-center"> | ||||
|         <div class="btn-group w-100"> | ||||
|   | ||||
| @@ -47,6 +47,7 @@ import { | ||||
|   FILTER_OWNER_DOES_NOT_INCLUDE, | ||||
|   FILTER_OWNER_ISNULL, | ||||
|   FILTER_CUSTOM_FIELDS, | ||||
|   FILTER_SHARED_BY_USER, | ||||
| } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| @@ -826,6 +827,16 @@ describe('FilterEditorComponent', () => { | ||||
|     expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy() | ||||
|   })) | ||||
|  | ||||
|   it('should ingest filter rules for shared by me', fakeAsync(() => { | ||||
|     component.filterRules = [ | ||||
|       { | ||||
|         rule_type: FILTER_SHARED_BY_USER, | ||||
|         value: '2', | ||||
|       }, | ||||
|     ] | ||||
|     expect(component.permissionsSelectionModel.userID).toEqual(2) | ||||
|   })) | ||||
|  | ||||
|   // GET filterRules | ||||
|  | ||||
|   it('should convert user input to correct filter rules on text field search title + content', fakeAsync(() => { | ||||
| @@ -1453,13 +1464,28 @@ describe('FilterEditorComponent', () => { | ||||
|     ]) | ||||
|   })) | ||||
|  | ||||
|   it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => { | ||||
|   it('should convert user input to correct filter on permissions select shared by me', fakeAsync(() => { | ||||
|     const permissionsDropdown = fixture.debugElement.query( | ||||
|       By.directive(PermissionsFilterDropdownComponent) | ||||
|     ) | ||||
|     const unownedButton = permissionsDropdown.queryAll(By.css('button'))[4] | ||||
|     unownedButton.triggerEventHandler('click') | ||||
|     fixture.detectChanges() | ||||
|     expect(component.filterRules).toEqual([ | ||||
|       { | ||||
|         rule_type: FILTER_SHARED_BY_USER, | ||||
|         value: '1', | ||||
|       }, | ||||
|     ]) | ||||
|   })) | ||||
|  | ||||
|   it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => { | ||||
|     const permissionsDropdown = fixture.debugElement.query( | ||||
|       By.directive(PermissionsFilterDropdownComponent) | ||||
|     ) | ||||
|     const unownedButton = permissionsDropdown.queryAll(By.css('button'))[5] | ||||
|     unownedButton.triggerEventHandler('click') | ||||
|     fixture.detectChanges() | ||||
|     expect(component.filterRules).toEqual([ | ||||
|       { | ||||
|         rule_type: FILTER_OWNER_ISNULL, | ||||
|   | ||||
| @@ -49,6 +49,7 @@ import { | ||||
|   FILTER_OWNER_ISNULL, | ||||
|   FILTER_OWNER_ANY, | ||||
|   FILTER_CUSTOM_FIELDS, | ||||
|   FILTER_SHARED_BY_USER, | ||||
| } from 'src/app/data/filter-rule-type' | ||||
| import { | ||||
|   FilterableDropdownSelectionModel, | ||||
| @@ -503,6 +504,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|               parseInt(rule.value, 10) | ||||
|             ) | ||||
|           break | ||||
|         case FILTER_SHARED_BY_USER: | ||||
|           this.permissionsSelectionModel.ownerFilter = | ||||
|             OwnerFilterType.SHARED_BY_ME | ||||
|           if (rule.value) | ||||
|             this.permissionsSelectionModel.userID = parseInt(rule.value, 10) | ||||
|           break | ||||
|         case FILTER_OWNER_ISNULL: | ||||
|           if (rule.value === 'true' || rule.value === '1') { | ||||
|             this.permissionsSelectionModel.hideUnowned = false | ||||
| @@ -801,6 +808,13 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|         rule_type: FILTER_OWNER_ANY, | ||||
|         value: this.permissionsSelectionModel.includeUsers?.join(','), | ||||
|       }) | ||||
|     } else if ( | ||||
|       this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SHARED_BY_ME | ||||
|     ) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_SHARED_BY_USER, | ||||
|         value: this.permissionsSelectionModel.userID.toString(), | ||||
|       }) | ||||
|     } else if ( | ||||
|       this.permissionsSelectionModel.ownerFilter == OwnerFilterType.UNOWNED | ||||
|     ) { | ||||
|   | ||||
| @@ -45,6 +45,7 @@ export const FILTER_OWNER = 32 | ||||
| export const FILTER_OWNER_ANY = 33 | ||||
| export const FILTER_OWNER_ISNULL = 34 | ||||
| export const FILTER_OWNER_DOES_NOT_INCLUDE = 35 | ||||
| export const FILTER_SHARED_BY_USER = 37 | ||||
|  | ||||
| export const FILTER_CUSTOM_FIELDS = 36 | ||||
|  | ||||
| @@ -273,6 +274,12 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'number', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_SHARED_BY_USER, | ||||
|     filtervar: 'shared_by__id', | ||||
|     datatype: 'number', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_CUSTOM_FIELDS, | ||||
|     filtervar: 'custom_fields__icontains', | ||||
|   | ||||
| @@ -17,4 +17,6 @@ export interface ObjectWithPermissions extends ObjectWithId { | ||||
|   permissions?: PermissionsObject | ||||
|  | ||||
|   user_can_change?: boolean | ||||
|  | ||||
|   is_shared_by_requester?: boolean | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,12 @@ | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.db.models import Count | ||||
| from django.db.models import OuterRef | ||||
| from django.db.models import Q | ||||
| from django_filters.rest_framework import BooleanFilter | ||||
| from django_filters.rest_framework import Filter | ||||
| from django_filters.rest_framework import FilterSet | ||||
| from guardian.utils import get_group_obj_perms_model | ||||
| from guardian.utils import get_user_obj_perms_model | ||||
| from rest_framework_guardian.filters import ObjectPermissionsFilter | ||||
|  | ||||
| from documents.models import Correspondent | ||||
| @@ -101,6 +106,39 @@ class TitleContentFilter(Filter): | ||||
|             return qs | ||||
|  | ||||
|  | ||||
| class SharedByUser(Filter): | ||||
|     def filter(self, qs, value): | ||||
|         ctype = ContentType.objects.get_for_model(self.model) | ||||
|         UserObjectPermission = get_user_obj_perms_model() | ||||
|         GroupObjectPermission = get_group_obj_perms_model() | ||||
|         return ( | ||||
|             qs.filter( | ||||
|                 owner_id=value, | ||||
|             ) | ||||
|             .annotate( | ||||
|                 num_shared_users=Count( | ||||
|                     UserObjectPermission.objects.filter( | ||||
|                         content_type=ctype, | ||||
|                         object_pk=OuterRef("pk"), | ||||
|                     ).values("user_id"), | ||||
|                 ), | ||||
|             ) | ||||
|             .annotate( | ||||
|                 num_shared_groups=Count( | ||||
|                     GroupObjectPermission.objects.filter( | ||||
|                         content_type=ctype, | ||||
|                         object_pk=OuterRef("pk"), | ||||
|                     ).values("group_id"), | ||||
|                 ), | ||||
|             ) | ||||
|             .filter( | ||||
|                 Q(num_shared_users__gt=0) | Q(num_shared_groups__gt=0), | ||||
|             ) | ||||
|             if value is not None | ||||
|             else qs | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class CustomFieldsFilter(Filter): | ||||
|     def filter(self, qs, value): | ||||
|         if value: | ||||
| @@ -144,6 +182,8 @@ class DocumentFilterSet(FilterSet): | ||||
|  | ||||
|     custom_fields__icontains = CustomFieldsFilter() | ||||
|  | ||||
|     shared_by__id = SharedByUser() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Document | ||||
|         fields = { | ||||
|   | ||||
| @@ -75,6 +75,7 @@ def get_schema(): | ||||
|         viewer_id=KEYWORD(commas=True), | ||||
|         checksum=TEXT(), | ||||
|         original_filename=TEXT(sortable=True), | ||||
|         is_shared=BOOLEAN(), | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @@ -167,6 +168,7 @@ def update_document(writer: AsyncWriter, doc: Document): | ||||
|         viewer_id=viewer_ids if viewer_ids else None, | ||||
|         checksum=doc.checksum, | ||||
|         original_filename=doc.original_filename, | ||||
|         is_shared=len(viewer_ids) > 0, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @@ -194,6 +196,7 @@ class DelayedQuery: | ||||
|         "document_type": ("type", ["id", "id__in", "id__none", "isnull"]), | ||||
|         "storage_path": ("path", ["id", "id__in", "id__none", "isnull"]), | ||||
|         "owner": ("owner", ["id", "id__in", "id__none", "isnull"]), | ||||
|         "shared_by": ("shared_by", ["id"]), | ||||
|         "tags": ("tag", ["id__all", "id__in", "id__none"]), | ||||
|         "added": ("added", ["date__lt", "date__gt"]), | ||||
|         "created": ("created", ["date__lt", "date__gt"]), | ||||
| @@ -233,6 +236,10 @@ class DelayedQuery: | ||||
|                 continue | ||||
|  | ||||
|             if query_filter == "id": | ||||
|                 if param == "shared_by": | ||||
|                     criterias.append(query.Term("is_shared", True)) | ||||
|                     criterias.append(query.Term("owner_id", value)) | ||||
|                 else: | ||||
|                     criterias.append(query.Term(f"{field}_id", value)) | ||||
|             elif query_filter == "id__in": | ||||
|                 in_filter = [] | ||||
|   | ||||
| @@ -0,0 +1,60 @@ | ||||
| # Generated by Django 4.2.7 on 2023-12-09 18:13 | ||||
|  | ||||
| from django.db import migrations | ||||
| from django.db import models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("documents", "1042_consumptiontemplate_assign_custom_fields_and_more"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="savedviewfilterrule", | ||||
|             name="rule_type", | ||||
|             field=models.PositiveIntegerField( | ||||
|                 choices=[ | ||||
|                     (0, "title contains"), | ||||
|                     (1, "content contains"), | ||||
|                     (2, "ASN is"), | ||||
|                     (3, "correspondent is"), | ||||
|                     (4, "document type is"), | ||||
|                     (5, "is in inbox"), | ||||
|                     (6, "has tag"), | ||||
|                     (7, "has any tag"), | ||||
|                     (8, "created before"), | ||||
|                     (9, "created after"), | ||||
|                     (10, "created year is"), | ||||
|                     (11, "created month is"), | ||||
|                     (12, "created day is"), | ||||
|                     (13, "added before"), | ||||
|                     (14, "added after"), | ||||
|                     (15, "modified before"), | ||||
|                     (16, "modified after"), | ||||
|                     (17, "does not have tag"), | ||||
|                     (18, "does not have ASN"), | ||||
|                     (19, "title or content contains"), | ||||
|                     (20, "fulltext query"), | ||||
|                     (21, "more like this"), | ||||
|                     (22, "has tags in"), | ||||
|                     (23, "ASN greater than"), | ||||
|                     (24, "ASN less than"), | ||||
|                     (25, "storage path is"), | ||||
|                     (26, "has correspondent in"), | ||||
|                     (27, "does not have correspondent in"), | ||||
|                     (28, "has document type in"), | ||||
|                     (29, "does not have document type in"), | ||||
|                     (30, "has storage path in"), | ||||
|                     (31, "does not have storage path in"), | ||||
|                     (32, "owner is"), | ||||
|                     (33, "has owner in"), | ||||
|                     (34, "does not have owner"), | ||||
|                     (35, "does not have owner in"), | ||||
|                     (36, "has custom field value"), | ||||
|                     (37, "is shared by me"), | ||||
|                 ], | ||||
|                 verbose_name="rule type", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -455,6 +455,8 @@ class SavedViewFilterRule(models.Model): | ||||
|         (33, _("has owner in")), | ||||
|         (34, _("does not have owner")), | ||||
|         (35, _("does not have owner in")), | ||||
|         (36, _("has custom field value")), | ||||
|         (37, _("is shared by me")), | ||||
|     ] | ||||
|  | ||||
|     saved_view = models.ForeignKey( | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from celery import states | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import Group | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.core.validators import URLValidator | ||||
| from django.utils.crypto import get_random_string | ||||
| from django.utils.text import slugify | ||||
| @@ -15,6 +16,8 @@ from django.utils.translation import gettext as _ | ||||
| from drf_writable_nested.serializers import NestedUpdateMixin | ||||
| from guardian.core import ObjectPermissionChecker | ||||
| from guardian.shortcuts import get_users_with_perms | ||||
| from guardian.utils import get_group_obj_perms_model | ||||
| from guardian.utils import get_user_obj_perms_model | ||||
| from rest_framework import fields | ||||
| from rest_framework import serializers | ||||
| from rest_framework.fields import SerializerMethodField | ||||
| @@ -160,6 +163,7 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin): | ||||
|         try: | ||||
|             if full_perms: | ||||
|                 self.fields.pop("user_can_change") | ||||
|                 self.fields.pop("is_shared_by_requester") | ||||
|             else: | ||||
|                 self.fields.pop("permissions") | ||||
|         except KeyError: | ||||
| @@ -205,8 +209,26 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin): | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     def get_is_shared_by_requester(self, obj: Document): | ||||
|         ctype = ContentType.objects.get_for_model(obj) | ||||
|         UserObjectPermission = get_user_obj_perms_model() | ||||
|         GroupObjectPermission = get_group_obj_perms_model() | ||||
|         return obj.owner == self.user and ( | ||||
|             UserObjectPermission.objects.filter( | ||||
|                 content_type=ctype, | ||||
|                 object_pk=obj.pk, | ||||
|             ).count() | ||||
|             > 0 | ||||
|             or GroupObjectPermission.objects.filter( | ||||
|                 content_type=ctype, | ||||
|                 object_pk=obj.pk, | ||||
|             ).count() | ||||
|             > 0 | ||||
|         ) | ||||
|  | ||||
|     permissions = SerializerMethodField(read_only=True) | ||||
|     user_can_change = SerializerMethodField(read_only=True) | ||||
|     is_shared_by_requester = SerializerMethodField(read_only=True) | ||||
|  | ||||
|     set_permissions = serializers.DictField( | ||||
|         label="Set permissions", | ||||
| @@ -556,6 +578,7 @@ class DocumentSerializer( | ||||
|             "owner", | ||||
|             "permissions", | ||||
|             "user_can_change", | ||||
|             "is_shared_by_requester", | ||||
|             "set_permissions", | ||||
|             "notes", | ||||
|             "custom_fields", | ||||
|   | ||||
| @@ -594,7 +594,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): | ||||
|         results = response.data["results"] | ||||
|         self.assertEqual(len(results), 0) | ||||
|  | ||||
|     def test_document_owner_filters(self): | ||||
|     def test_document_permissions_filters(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Documents with owners, with and without granted permissions | ||||
| @@ -686,6 +686,18 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): | ||||
|             [u1_doc1.id, u1_doc2.id, u2_doc2.id], | ||||
|         ) | ||||
|  | ||||
|         assign_perm("view_document", u2, u1_doc1) | ||||
|  | ||||
|         # Will show only documents shared by user | ||||
|         response = self.client.get(f"/api/documents/?shared_by__id={u1.id}") | ||||
|         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||
|         results = response.data["results"] | ||||
|         self.assertEqual(len(results), 1) | ||||
|         self.assertCountEqual( | ||||
|             [results[0]["id"]], | ||||
|             [u1_doc1.id], | ||||
|         ) | ||||
|  | ||||
|     def test_pagination_all(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
|   | ||||
| @@ -408,10 +408,17 @@ class TestApiAuth(DirectoriesMixin, APITestCase): | ||||
|             checksum="3", | ||||
|             owner=user2, | ||||
|         ) | ||||
|         doc4 = Document.objects.create( | ||||
|             title="Test4", | ||||
|             content="content 4", | ||||
|             checksum="4", | ||||
|             owner=user1, | ||||
|         ) | ||||
|  | ||||
|         assign_perm("view_document", user1, doc2) | ||||
|         assign_perm("view_document", user1, doc3) | ||||
|         assign_perm("change_document", user1, doc3) | ||||
|         assign_perm("view_document", user2, doc4) | ||||
|  | ||||
|         self.client.force_authenticate(user1) | ||||
|  | ||||
| @@ -426,9 +433,11 @@ class TestApiAuth(DirectoriesMixin, APITestCase): | ||||
|  | ||||
|         self.assertNotIn("permissions", resp_data["results"][0]) | ||||
|         self.assertIn("user_can_change", resp_data["results"][0]) | ||||
|         self.assertEqual(resp_data["results"][0]["user_can_change"], True)  # doc1 | ||||
|         self.assertEqual(resp_data["results"][1]["user_can_change"], False)  # doc2 | ||||
|         self.assertEqual(resp_data["results"][2]["user_can_change"], True)  # doc3 | ||||
|         self.assertTrue(resp_data["results"][0]["user_can_change"])  # doc1 | ||||
|         self.assertFalse(resp_data["results"][0]["is_shared_by_requester"])  # doc1 | ||||
|         self.assertFalse(resp_data["results"][1]["user_can_change"])  # doc2 | ||||
|         self.assertTrue(resp_data["results"][2]["user_can_change"])  # doc3 | ||||
|         self.assertTrue(resp_data["results"][3]["is_shared_by_requester"])  # doc4 | ||||
|  | ||||
|         response = self.client.get( | ||||
|             "/api/documents/?full_perms=true", | ||||
| @@ -441,6 +450,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase): | ||||
|  | ||||
|         self.assertIn("permissions", resp_data["results"][0]) | ||||
|         self.assertNotIn("user_can_change", resp_data["results"][0]) | ||||
|         self.assertNotIn("is_shared_by_requester", resp_data["results"][0]) | ||||
|  | ||||
|  | ||||
| class TestApiUser(DirectoriesMixin, APITestCase): | ||||
|   | ||||
| @@ -968,7 +968,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): | ||||
|         u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) | ||||
|         u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) | ||||
|  | ||||
|         Document.objects.create(checksum="1", content="test 1", owner=u1) | ||||
|         d1 = Document.objects.create(checksum="1", content="test 1", owner=u1) | ||||
|         d2 = Document.objects.create(checksum="2", content="test 2", owner=u2) | ||||
|         d3 = Document.objects.create(checksum="3", content="test 3", owner=u2) | ||||
|         Document.objects.create(checksum="4", content="test 4") | ||||
| @@ -993,9 +993,10 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): | ||||
|  | ||||
|         assign_perm("view_document", u1, d2) | ||||
|         assign_perm("view_document", u1, d3) | ||||
|         assign_perm("view_document", u2, d1) | ||||
|  | ||||
|         with AsyncWriter(index.open_index()) as writer: | ||||
|             for doc in [d2, d3]: | ||||
|             for doc in [d1, d2, d3]: | ||||
|                 index.update_document(writer, doc) | ||||
|  | ||||
|         self.client.force_authenticate(user=u1) | ||||
| @@ -1011,6 +1012,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): | ||||
|         self.assertEqual(r.data["count"], 1) | ||||
|         r = self.client.get("/api/documents/?query=test&owner__isnull=true") | ||||
|         self.assertEqual(r.data["count"], 1) | ||||
|         r = self.client.get(f"/api/documents/?query=test&shared_by__id={u1.id}") | ||||
|         self.assertEqual(r.data["count"], 1) | ||||
|  | ||||
|     def test_search_sorting(self): | ||||
|         u1 = User.objects.create_user("user1") | ||||
|   | ||||
| @@ -2,7 +2,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: paperless-ngx\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2023-12-05 08:26-0800\n" | ||||
| "POT-Creation-Date: 2023-12-09 10:53-0800\n" | ||||
| "PO-Revision-Date: 2022-02-17 04:17\n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: English\n" | ||||
| @@ -21,7 +21,7 @@ msgstr "" | ||||
| msgid "Documents" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:36 documents/models.py:734 | ||||
| #: documents/models.py:36 documents/models.py:736 | ||||
| msgid "owner" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -53,7 +53,7 @@ msgstr "" | ||||
| msgid "Automatic" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:62 documents/models.py:402 documents/models.py:895 | ||||
| #: documents/models.py:62 documents/models.py:402 documents/models.py:897 | ||||
| #: paperless_mail/models.py:18 paperless_mail/models.py:93 | ||||
| msgid "name" | ||||
| msgstr "" | ||||
| @@ -132,7 +132,7 @@ msgstr "" | ||||
| msgid "title" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:171 documents/models.py:648 | ||||
| #: documents/models.py:171 documents/models.py:650 | ||||
| msgid "content" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -162,8 +162,8 @@ msgstr "" | ||||
| msgid "The checksum of the archived document." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:205 documents/models.py:385 documents/models.py:654 | ||||
| #: documents/models.py:692 documents/models.py:762 documents/models.py:799 | ||||
| #: documents/models.py:205 documents/models.py:385 documents/models.py:656 | ||||
| #: documents/models.py:694 documents/models.py:764 documents/models.py:801 | ||||
| msgid "created" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -211,7 +211,7 @@ msgstr "" | ||||
| msgid "The position of this document in your physical document archive." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:279 documents/models.py:665 documents/models.py:719 | ||||
| #: documents/models.py:279 documents/models.py:667 documents/models.py:721 | ||||
| msgid "document" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -259,7 +259,7 @@ msgstr "" | ||||
| msgid "logs" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:399 documents/models.py:464 | ||||
| #: documents/models.py:399 documents/models.py:466 | ||||
| msgid "saved view" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -427,298 +427,306 @@ msgstr "" | ||||
| msgid "does not have owner in" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:467 | ||||
| msgid "rule type" | ||||
| #: documents/models.py:458 | ||||
| msgid "has custom field value" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:459 | ||||
| msgid "is shared by me" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:469 | ||||
| msgid "rule type" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:471 | ||||
| msgid "value" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:472 | ||||
| #: documents/models.py:474 | ||||
| msgid "filter rule" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:473 | ||||
| #: documents/models.py:475 | ||||
| msgid "filter rules" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:584 | ||||
| #: documents/models.py:586 | ||||
| msgid "Task ID" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:585 | ||||
| #: documents/models.py:587 | ||||
| msgid "Celery ID for the Task that was run" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:590 | ||||
| #: documents/models.py:592 | ||||
| msgid "Acknowledged" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:591 | ||||
| #: documents/models.py:593 | ||||
| msgid "If the task is acknowledged via the frontend or API" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:597 | ||||
| #: documents/models.py:599 | ||||
| msgid "Task Filename" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:598 | ||||
| #: documents/models.py:600 | ||||
| msgid "Name of the file which the Task was run for" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:604 | ||||
| #: documents/models.py:606 | ||||
| msgid "Task Name" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:605 | ||||
| #: documents/models.py:607 | ||||
| msgid "Name of the Task which was run" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:612 | ||||
| #: documents/models.py:614 | ||||
| msgid "Task State" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:613 | ||||
| #: documents/models.py:615 | ||||
| msgid "Current state of the task being run" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:618 | ||||
| #: documents/models.py:620 | ||||
| msgid "Created DateTime" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:619 | ||||
| #: documents/models.py:621 | ||||
| msgid "Datetime field when the task result was created in UTC" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:624 | ||||
| #: documents/models.py:626 | ||||
| msgid "Started DateTime" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:625 | ||||
| #: documents/models.py:627 | ||||
| msgid "Datetime field when the task was started in UTC" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:630 | ||||
| #: documents/models.py:632 | ||||
| msgid "Completed DateTime" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:631 | ||||
| #: documents/models.py:633 | ||||
| msgid "Datetime field when the task was completed in UTC" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:636 | ||||
| #: documents/models.py:638 | ||||
| msgid "Result Data" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:638 | ||||
| #: documents/models.py:640 | ||||
| msgid "The data returned by the task" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:650 | ||||
| #: documents/models.py:652 | ||||
| msgid "Note for the document" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:674 | ||||
| #: documents/models.py:676 | ||||
| msgid "user" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:679 | ||||
| #: documents/models.py:681 | ||||
| msgid "note" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:680 | ||||
| #: documents/models.py:682 | ||||
| msgid "notes" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:688 | ||||
| #: documents/models.py:690 | ||||
| msgid "Archive" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:689 | ||||
| #: documents/models.py:691 | ||||
| msgid "Original" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:700 | ||||
| #: documents/models.py:702 | ||||
| msgid "expiration" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:707 | ||||
| #: documents/models.py:709 | ||||
| msgid "slug" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:739 | ||||
| #: documents/models.py:741 | ||||
| msgid "share link" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:740 | ||||
| #: documents/models.py:742 | ||||
| msgid "share links" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:752 | ||||
| #: documents/models.py:754 | ||||
| msgid "String" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:753 | ||||
| #: documents/models.py:755 | ||||
| msgid "URL" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:754 | ||||
| #: documents/models.py:756 | ||||
| msgid "Date" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:755 | ||||
| #: documents/models.py:757 | ||||
| msgid "Boolean" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:756 | ||||
| #: documents/models.py:758 | ||||
| msgid "Integer" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:757 | ||||
| #: documents/models.py:759 | ||||
| msgid "Float" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:758 | ||||
| #: documents/models.py:760 | ||||
| msgid "Monetary" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:759 | ||||
| #: documents/models.py:761 | ||||
| msgid "Document Link" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:771 | ||||
| #: documents/models.py:773 | ||||
| msgid "data type" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:779 | ||||
| #: documents/models.py:781 | ||||
| msgid "custom field" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:780 | ||||
| #: documents/models.py:782 | ||||
| msgid "custom fields" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:842 | ||||
| #: documents/models.py:844 | ||||
| msgid "custom field instance" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:843 | ||||
| #: documents/models.py:845 | ||||
| msgid "custom field instances" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:891 | ||||
| #: documents/models.py:893 | ||||
| msgid "Consume Folder" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:892 | ||||
| #: documents/models.py:894 | ||||
| msgid "Api Upload" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:893 | ||||
| #: documents/models.py:895 | ||||
| msgid "Mail Fetch" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:897 paperless_mail/models.py:95 | ||||
| #: documents/models.py:899 paperless_mail/models.py:95 | ||||
| msgid "order" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:906 | ||||
| #: documents/models.py:908 | ||||
| msgid "filter path" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:911 | ||||
| #: documents/models.py:913 | ||||
| msgid "" | ||||
| "Only consume documents with a path that matches this if specified. Wildcards " | ||||
| "specified as * are allowed. Case insensitive." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:918 | ||||
| #: documents/models.py:920 | ||||
| msgid "filter filename" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:923 paperless_mail/models.py:148 | ||||
| #: documents/models.py:925 paperless_mail/models.py:148 | ||||
| msgid "" | ||||
| "Only consume documents which entirely match this filename if specified. " | ||||
| "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:934 | ||||
| #: documents/models.py:936 | ||||
| msgid "filter documents from this mail rule" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:938 | ||||
| #: documents/models.py:940 | ||||
| msgid "assign title" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:943 | ||||
| #: documents/models.py:945 | ||||
| msgid "" | ||||
| "Assign a document title, can include some placeholders, see documentation." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:951 paperless_mail/models.py:216 | ||||
| #: documents/models.py:953 paperless_mail/models.py:216 | ||||
| msgid "assign this tag" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:959 paperless_mail/models.py:224 | ||||
| #: documents/models.py:961 paperless_mail/models.py:224 | ||||
| msgid "assign this document type" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:967 paperless_mail/models.py:238 | ||||
| #: documents/models.py:969 paperless_mail/models.py:238 | ||||
| msgid "assign this correspondent" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:975 | ||||
| #: documents/models.py:977 | ||||
| msgid "assign this storage path" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:984 | ||||
| #: documents/models.py:986 | ||||
| msgid "assign this owner" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:991 | ||||
| #: documents/models.py:993 | ||||
| msgid "grant view permissions to these users" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:998 | ||||
| #: documents/models.py:1000 | ||||
| msgid "grant view permissions to these groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1005 | ||||
| #: documents/models.py:1007 | ||||
| msgid "grant change permissions to these users" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1012 | ||||
| #: documents/models.py:1014 | ||||
| msgid "grant change permissions to these groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1019 | ||||
| #: documents/models.py:1021 | ||||
| msgid "assign these custom fields" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1023 | ||||
| #: documents/models.py:1025 | ||||
| msgid "consumption template" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1024 | ||||
| #: documents/models.py:1026 | ||||
| msgid "consumption templates" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:102 | ||||
| #: documents/serialisers.py:105 | ||||
| #, python-format | ||||
| msgid "Invalid regular expression: %(error)s" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:377 | ||||
| #: documents/serialisers.py:399 | ||||
| msgid "Invalid color." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:842 | ||||
| #: documents/serialisers.py:865 | ||||
| #, python-format | ||||
| msgid "File type %(type)s not supported" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:939 | ||||
| #: documents/serialisers.py:962 | ||||
| msgid "Invalid variable detected." | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon