</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
- <context context-type="linenumber">29</context>
+ <context context-type="linenumber">79</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context>
<context context-type="linenumber">35</context>
</context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
+ <context context-type="linenumber">50</context>
+ </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context>
<context context-type="linenumber">51</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
- <context context-type="linenumber">28</context>
+ <context context-type="linenumber">78</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
- <context context-type="linenumber">14</context>
+ <context context-type="linenumber">64</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
- <context context-type="linenumber">16</context>
+ <context context-type="linenumber">66</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
- <context context-type="linenumber">19</context>
+ <context context-type="linenumber">69</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
- <trans-unit id="6625768491622252297" datatype="html">
- <source>e.g.</source>
+ <trans-unit id="2816147949408898105" datatype="html">
+ <source>See <a target='_blank' href='https://docs.paperless-ngx.com/advanced_usage/#file-name-handling'>the documentation</a>.</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts</context>
- <context context-type="linenumber">28</context>
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
+ <context context-type="linenumber">13</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="1295614462098694869" datatype="html">
+ <source>Preview</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
+ <context context-type="linenumber">18</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
+ <context context-type="linenumber">282</context>
</context-group>
</trans-unit>
- <trans-unit id="1918584360573970155" datatype="html">
- <source>or use slashes to add directories e.g.</source>
+ <trans-unit id="8057014866157903311" datatype="html">
+ <source>Path test failed</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts</context>
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
- <trans-unit id="7871464228487558644" datatype="html">
- <source>See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">documentation</a> for full list.</source>
+ <trans-unit id="9116034231465034307" datatype="html">
+ <source>No document selected</source>
<context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts</context>
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
+ <trans-unit id="5676637575587497817" datatype="html">
+ <source>Search for documents</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
+ <context context-type="linenumber">38</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
+ <context context-type="linenumber">53</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="6423278459497515329" datatype="html">
+ <source>No documents found</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
+ <context context-type="linenumber">39</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
+ <context context-type="linenumber">44</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="6898961890896270754" datatype="html">
<source>Create new storage path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts</context>
- <context context-type="linenumber">37</context>
+ <context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="3754859110054016570" datatype="html">
<source>Edit storage path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts</context>
- <context context-type="linenumber">41</context>
+ <context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="9011959596901584887" datatype="html">
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
- <trans-unit id="6423278459497515329" datatype="html">
- <source>No documents found</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
- <context context-type="linenumber">44</context>
- </context-group>
- </trans-unit>
- <trans-unit id="5676637575587497817" datatype="html">
- <source>Search for documents</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
- <context context-type="linenumber">53</context>
- </context-group>
- </trans-unit>
<trans-unit id="8627133593113147800" datatype="html">
<source>Selected items</source>
<context-group purpose="location">
<context context-type="linenumber">275</context>
</context-group>
</trans-unit>
- <trans-unit id="1295614462098694869" datatype="html">
- <source>Preview</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
- <context context-type="linenumber">282</context>
- </context-group>
- </trans-unit>
<trans-unit id="7206723502037428235" datatype="html">
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
<context-group purpose="location">
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
- <pngx-input-textarea i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint" [monospace]="true"></pngx-input-textarea>
+ <pngx-input-textarea i18n-title title="Path" formControlName="path" [error]="error?.path" hint="See <a target='_blank' href='https://docs.paperless-ngx.com/advanced_usage/#file-name-handling'>the documentation</a>." i18n-hint [monospace]="true"></pngx-input-textarea>
+
+ <div ngbAccordion>
+ <div ngbAccordionItem>
+ <h2 ngbAccordionHeader>
+ <button ngbAccordionButton i18n>Preview</button>
+ </h2>
+ <div ngbAccordionCollapse>
+ <div ngbAccordionBody>
+ <ng-template>
+ <div class="card mb-2">
+ <div class="card-body p-2">
+ @if (testLoading) {
+ <ng-container [ngTemplateOutlet]="loadingTemplate"></ng-container>
+ } @else if (testResult) {
+ <code>{{testResult}}</code>
+ } @else if (testFailed) {
+ <div class="text-danger" i18n>Path test failed</div>
+ } @else {
+ <div class="text-muted small" i18n>No document selected</div>
+ }
+ </div>
+ </div>
+ <ng-select name="testDocument"
+ [items]="foundDocuments$ | async"
+ placeholder="Search for a document" i18n-placeholder
+ notFoundText="No documents found" i18n-notFoundText
+ bindValue="id"
+ bindLabel="title"
+ [compareWith]="compareDocuments"
+ [trackByFn]="trackByFn"
+ [minTermLength]="2"
+ [loading]="loading"
+ [typeahead]="documentsInput$"
+ (change)="testPath($event)">
+ <ng-template #loadingTemplate ng-loadingspinner-tmp>
+ <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
+ <div class="visually-hidden" i18n>Loading...</div>
+ </ng-template>
+ <ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
+ <div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
+ </ng-template>
+ </ng-select>
+ </ng-template>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr/>
+
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
+.accordion {
+ --bs-accordion-btn-padding-x: 0.75rem;
+ --bs-accordion-btn-padding-y: 0.375rem;
+ }
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
-import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import {
+ NgbAccordionButton,
+ NgbActiveModal,
+ NgbModule,
+} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { EditDialogMode } from '../edit-dialog.component'
import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
+import { StoragePathService } from 'src/app/services/rest/storage-path.service'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { of, throwError } from 'rxjs'
+import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
+import { By } from '@angular/platform-browser'
describe('StoragePathEditDialogComponent', () => {
let component: StoragePathEditDialogComponent
let settingsService: SettingsService
+ let documentService: DocumentService
let fixture: ComponentFixture<StoragePathEditDialogComponent>
beforeEach(async () => {
],
}).compileComponents()
+ documentService = TestBed.inject(DocumentService)
fixture = TestBed.createComponent(StoragePathEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
+
+ it('should support test path', () => {
+ const testSpy = jest.spyOn(
+ component['service'] as StoragePathService,
+ 'testPath'
+ )
+ testSpy.mockReturnValueOnce(of('test/abc123'))
+ component.objectForm.patchValue({ path: 'test/{{title}}' })
+ fixture.detectChanges()
+ component.testPath({ id: 1 })
+ expect(testSpy).toHaveBeenCalledWith('test/{{title}}', 1)
+ expect(component.testResult).toBe('test/abc123')
+ expect(component.testFailed).toBeFalsy()
+
+ // test failed
+ testSpy.mockReturnValueOnce(of(''))
+ component.testPath({ id: 1 })
+ expect(component.testResult).toBeNull()
+ expect(component.testFailed).toBeTruthy()
+
+ component.testPath(null)
+ expect(component.testResult).toBeNull()
+ })
+
+ it('should compare two documents by id', () => {
+ const doc1 = { id: 1 }
+ const doc2 = { id: 2 }
+ expect(component.compareDocuments(doc1, doc1)).toBeTruthy()
+ expect(component.compareDocuments(doc1, doc2)).toBeFalsy()
+ })
+
+ it('should use id as trackBy', () => {
+ expect(component.trackByFn({ id: 1 })).toBe(1)
+ })
+
+ it('should search on select text input', () => {
+ fixture.debugElement
+ .query(By.directive(NgbAccordionButton))
+ .triggerEventHandler('click', null)
+ fixture.detectChanges()
+ const documents = [
+ { id: 1, title: 'foo' },
+ { id: 2, title: 'bar' },
+ ]
+ const listSpy = jest.spyOn(documentService, 'listFiltered')
+ listSpy.mockReturnValueOnce(
+ of({
+ count: 1,
+ results: documents[0],
+ all: [1],
+ } as any)
+ )
+ component.documentsInput$.next('bar')
+ expect(listSpy).toHaveBeenCalledWith(
+ 1,
+ null,
+ 'created',
+ true,
+ [{ rule_type: FILTER_TITLE, value: 'bar' }],
+ { truncate_content: true }
+ )
+ listSpy.mockReturnValueOnce(
+ of({
+ count: 2,
+ results: [...documents],
+ all: [1, 2],
+ } as any)
+ )
+ component.documentsInput$.next('ba')
+ listSpy.mockReturnValueOnce(throwError(() => new Error()))
+ component.documentsInput$.next('foo')
+ })
+
+ it('should run path test on path change', () => {
+ const testSpy = jest.spyOn(component, 'testPath')
+ component['testDocument'] = { id: 1 } as any
+ component.objectForm.patchValue(
+ { path: 'test/{{title}}' },
+ { emitEvent: true }
+ )
+ fixture.detectChanges()
+ expect(testSpy).toHaveBeenCalled()
+ })
})
-import { Component } from '@angular/core'
+import { Component, OnDestroy } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import {
+ Subject,
+ Observable,
+ concat,
+ of,
+ distinctUntilChanged,
+ takeUntil,
+ tap,
+ switchMap,
+ map,
+ catchError,
+ filter,
+} from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
+import { Document } from 'src/app/data/document'
+import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { StoragePath } from 'src/app/data/storage-path'
+import { DocumentService } from 'src/app/services/rest/document.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './storage-path-edit-dialog.component.html',
styleUrls: ['./storage-path-edit-dialog.component.scss'],
})
-export class StoragePathEditDialogComponent extends EditDialogComponent<StoragePath> {
+export class StoragePathEditDialogComponent
+ extends EditDialogComponent<StoragePath>
+ implements OnDestroy
+{
+ public documentsInput$ = new Subject<string>()
+ public foundDocuments$: Observable<Document[]>
+ private testDocument: Document
+ public testResult: string
+ public testFailed: boolean = false
+ public loading = false
+ public testLoading = false
+
+ private unsubscribeNotifier: Subject<any> = new Subject()
+
constructor(
service: StoragePathService,
activeModal: NgbActiveModal,
userService: UserService,
- settingsService: SettingsService
+ settingsService: SettingsService,
+ private documentsService: DocumentService
) {
super(service, activeModal, userService, settingsService)
+ this.initPathObservables()
}
- get pathHint() {
- return (
- $localize`e.g.` +
- ' <code class="text-nowrap">{{ created_year }}-{{ title }}</code> ' +
- $localize`or use slashes to add directories e.g.` +
- ' <code class="text-nowrap">{{ created_year }}/{{ title }}</code>. ' +
- $localize`See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">documentation</a> for full list.`
- )
+ ngOnDestroy(): void {
+ this.unsubscribeNotifier.next(this)
+ this.unsubscribeNotifier.complete()
}
getCreateTitle() {
permissions_form: new FormControl(null),
})
}
+
+ public testPath(document: Document) {
+ if (!document) {
+ this.testResult = null
+ return
+ }
+ this.testDocument = document
+ this.testLoading = true
+ ;(this.service as StoragePathService)
+ .testPath(this.objectForm.get('path').value, document.id)
+ .subscribe((result) => {
+ if (result?.length) {
+ this.testResult = result
+ this.testFailed = false
+ } else {
+ this.testResult = null
+ this.testFailed = true
+ }
+ this.testLoading = false
+ })
+ }
+
+ compareDocuments(document: Document, selectedDocument: Document) {
+ return document.id === selectedDocument.id
+ }
+
+ private initPathObservables() {
+ this.objectForm
+ .get('path')
+ .valueChanges.pipe(
+ takeUntil(this.unsubscribeNotifier),
+ filter((path) => path && !!this.testDocument)
+ )
+ .subscribe(() => {
+ this.testPath(this.testDocument)
+ })
+
+ this.foundDocuments$ = concat(
+ of([]), // default items
+ this.documentsInput$.pipe(
+ tap(() => console.log('searching')),
+ distinctUntilChanged(),
+ takeUntil(this.unsubscribeNotifier),
+ tap(() => (this.loading = true)),
+ switchMap((title) =>
+ this.documentsService
+ .listFiltered(
+ 1,
+ null,
+ 'created',
+ true,
+ [{ rule_type: FILTER_TITLE, value: title }],
+ { truncate_content: true }
+ )
+ .pipe(
+ map((result) => result.results),
+ catchError(() => of([])), // empty on error
+ tap(() => (this.loading = false))
+ )
+ )
+ )
+ )
+ }
+
+ trackByFn(item: Document) {
+ return item.id
+ }
}
color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;
}
}
+
+.accordion-button {
+ font-size: 1rem;
+}
(change)="onChange(value)"
[disabled]="disabled"
[placeholder]="placeholder"
- rows="6">
+ rows="4">
</textarea>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
import { StoragePathService } from './storage-path.service'
import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec'
+import { Subscription } from 'rxjs'
+import { HttpTestingController } from '@angular/common/http/testing'
+import { TestBed } from '@angular/core/testing'
+import { environment } from 'src/environments/environment'
+
+let httpTestingController: HttpTestingController
+let service: StoragePathService
+let subscription: Subscription
+const endpoint = 'storage_paths'
commonAbstractNameFilterPaperlessServiceTests(
'storage_paths',
StoragePathService
)
+
+describe(`Additional service tests for StoragePathservice`, () => {
+ beforeEach(() => {
+ httpTestingController = TestBed.inject(HttpTestingController)
+ service = TestBed.inject(StoragePathService)
+ })
+
+ afterEach(() => {
+ subscription?.unsubscribe()
+ httpTestingController.verify()
+ })
+
+ it('should support testing path', () => {
+ subscription = service.testPath('path', 11).subscribe()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}${endpoint}/test/`)
+ .flush('ok')
+ })
+})
import { Injectable } from '@angular/core'
import { StoragePath } from 'src/app/data/storage-path'
import { AbstractNameFilterService } from './abstract-name-filter-service'
+import { Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
constructor(http: HttpClient) {
super(http, 'storage_paths')
}
+
+ public testPath(path: string, documentID: number): Observable<any> {
+ return this.http.post<string>(`${this.getResourceUrl()}test/`, {
+ path,
+ document: documentID,
+ })
+ }
}
// Paperless-ngx styles
body {
- font-size: 0.875rem;
+ --pngx-body-font-size: 0.875rem;
+ font-size: var(--pngx-body-font-size);
height: 100vh;
}
filter: invert(0.5) saturate(0);
}
+.accordion-button {
+ font-size: var(--pngx-body-font-size);
+}
+
.me-1px {
margin-right: 1px !important;
}
"Some documents in the list have not yet been deleted.",
)
return documents
+
+
+class StoragePathTestSerializer(SerializerWithPerms):
+ path = serializers.CharField(
+ required=True,
+ label="Path",
+ write_only=True,
+ )
+
+ document = serializers.PrimaryKeyRelatedField(
+ queryset=Document.objects.all(),
+ required=True,
+ label="Document",
+ write_only=True,
+ )
)
# String types need to be sanitized
if field_instance.field.data_type in {
- CustomField.FieldDataType.DOCUMENTLINK,
CustomField.FieldDataType.MONETARY,
CustomField.FieldDataType.STRING,
CustomField.FieldDataType.URL,
# only called once
bulk_update_mock.assert_called_once_with([document.pk])
+ def test_test_storage_path(self):
+ """
+ GIVEN:
+ - API request to test a storage path
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ - Correct response data
+ """
+ document = Document.objects.create(
+ mime_type="application/pdf",
+ storage_path=self.sp1,
+ title="Something",
+ checksum="123",
+ )
+ response = self.client.post(
+ f"{self.ENDPOINT}test/",
+ json.dumps(
+ {
+ "document": document.id,
+ "path": "path/{{ title }}",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, "path/Something")
+
class TestBulkEditObjects(APITestCase):
# See test_api_permissions.py for bulk tests on permissions
from documents.serialisers import SearchResultSerializer
from documents.serialisers import ShareLinkSerializer
from documents.serialisers import StoragePathSerializer
+from documents.serialisers import StoragePathTestSerializer
from documents.serialisers import TagSerializer
from documents.serialisers import TagSerializerVersion1
from documents.serialisers import TasksViewSerializer
from documents.signals import document_updated
from documents.tasks import consume_file
from documents.tasks import empty_trash
+from documents.templating.filepath import validate_filepath_template_and_render
from paperless import version
from paperless.celery import app as celery_app
from paperless.config import GeneralConfig
return response
+class StoragePathTestView(GenericAPIView):
+ """
+ Test storage path against a document
+ """
+
+ permission_classes = [IsAuthenticated]
+ serializer_class = StoragePathTestSerializer
+
+ def post(self, request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ document = serializer.validated_data.get("document")
+ path = serializer.validated_data.get("path")
+
+ result = validate_filepath_template_and_render(path, document)
+ return Response(result)
+
+
class UiSettingsView(GenericAPIView):
queryset = UiSettings.objects.all()
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
from documents.views import SharedLinkView
from documents.views import ShareLinkViewSet
from documents.views import StatisticsView
+from documents.views import StoragePathTestView
from documents.views import StoragePathViewSet
from documents.views import SystemStatusView
from documents.views import TagViewSet
TrashView.as_view(),
name="trash",
),
+ re_path(
+ "^storage_paths/test/",
+ StoragePathTestView.as_view(),
+ name="storage_paths_test",
+ ),
*api_router.urls,
],
),