mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Enhancement: shared icon & shared by me filter (#4859)
This commit is contained in:
parent
088bad9030
commit
5e8de4c1da
@ -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,7 +236,11 @@ class DelayedQuery:
|
||||
continue
|
||||
|
||||
if query_filter == "id":
|
||||
criterias.append(query.Term(f"{field}_id", value))
|
||||
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 = []
|
||||
for object_id in value.split(","):
|
||||
|
@ -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 ""
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user