mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge branch 'feature-bulk-edit' into feature-bulk-editor
This commit is contained in:
		| @@ -5,6 +5,7 @@ | ||||
|       [disabled]="disabled" | ||||
|       [style.color]="textColor" | ||||
|       [style.background]="backgroundColor" | ||||
|       [clearable]="allowNull" | ||||
|       (change)="onChange(value)" | ||||
|       (blur)="onTouched()"> | ||||
|       <ng-option *ngFor="let i of items" [value]="i.id">{{i.name}}</ng-option> | ||||
|   | ||||
| @@ -67,9 +67,9 @@ | ||||
|                                 formControlName='archive_serial_number'> | ||||
|                         </div> | ||||
|                         <app-input-date-time titleDate="Date created" formControlName="created"></app-input-date-time> | ||||
|                         <app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent" | ||||
|                         <app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent" [allowNull]="true" | ||||
|                             (createNew)="createCorrespondent()"></app-input-select> | ||||
|                         <app-input-select [items]="documentTypes" title="Document type" formControlName="document_type" | ||||
|                         <app-input-select [items]="documentTypes" title="Document type" formControlName="document_type" [allowNull]="true" | ||||
|                             (createNew)="createDocumentType()"></app-input-select> | ||||
|                         <app-input-tags formControlName="tags" title="Tags"></app-input-tags> | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { map } from 'rxjs/operators'; | ||||
| import { tap } from 'rxjs/operators'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| @@ -16,10 +16,8 @@ import { FilterEditorComponent } from './filter-editor/filter-editor.component'; | ||||
| import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'; | ||||
| import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component'; | ||||
| import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { ChangedItems } from './bulk-editor/bulk-editor.component'; | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-list', | ||||
| @@ -38,7 +36,8 @@ export class DocumentListComponent implements OnInit { | ||||
|     private correspondentService: CorrespondentService, | ||||
|     private documentTypeService: DocumentTypeService, | ||||
|     private tagService: TagService, | ||||
|     private documentService: DocumentService) { } | ||||
|     private documentService: DocumentService, | ||||
|     private openDocumentService: OpenDocumentsService) { } | ||||
|  | ||||
|   @ViewChild("filterEditor") | ||||
|   private filterEditor: FilterEditorComponent | ||||
| @@ -147,12 +146,12 @@ export class DocumentListComponent implements OnInit { | ||||
|  | ||||
|   private executeBulkOperation(method: string, args): Observable<any> { | ||||
|     return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe( | ||||
|       map(r => { | ||||
|  | ||||
|       tap(() => { | ||||
|         this.list.reload() | ||||
|         this.list.selected.forEach(id => { | ||||
|           this.openDocumentService.refreshDocument(id) | ||||
|         }) | ||||
|         this.list.selectNone() | ||||
|  | ||||
|         return r | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { PaperlessDocument } from '../data/paperless-document'; | ||||
| import { OPEN_DOCUMENT_SERVICE } from '../data/storage-keys'; | ||||
| import { DocumentService } from './rest/document.service'; | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| @@ -9,7 +10,7 @@ export class OpenDocumentsService { | ||||
|  | ||||
|   private MAX_OPEN_DOCUMENTS = 5 | ||||
|  | ||||
|   constructor() {  | ||||
|   constructor(private documentService: DocumentService) {  | ||||
|     if (sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)) { | ||||
|       try { | ||||
|         this.openDocuments = JSON.parse(sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)) | ||||
| @@ -22,6 +23,15 @@ export class OpenDocumentsService { | ||||
|  | ||||
|   private openDocuments: PaperlessDocument[] = [] | ||||
|  | ||||
|   refreshDocument(id: number) { | ||||
|     let index = this.openDocuments.findIndex(doc => doc.id == id) | ||||
|     if (index > -1) { | ||||
|       this.documentService.get(id).subscribe(doc => { | ||||
|         this.openDocuments[index] = doc | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getOpenDocuments(): PaperlessDocument[] { | ||||
|     return this.openDocuments | ||||
|   } | ||||
|   | ||||
| @@ -51,6 +51,6 @@ def parser_check(app_configs, **kwargs): | ||||
|  | ||||
|     if len(parsers) == 0: | ||||
|         return [Error("No parsers found. This is a bug. The consumer won't be " | ||||
|                       "able to onsume any documents without parsers.")] | ||||
|                       "able to consume any documents without parsers.")] | ||||
|     else: | ||||
|         return [] | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from django.core.management import BaseCommand | ||||
| from django.db import transaction | ||||
|  | ||||
| from documents.mixins import Renderable | ||||
| from documents.tasks import index_reindex, index_optimize | ||||
| @@ -18,8 +19,8 @@ class Command(Renderable, BaseCommand): | ||||
|     def handle(self, *args, **options): | ||||
|  | ||||
|         self.verbosity = options["verbosity"] | ||||
|  | ||||
|         if options['command'] == 'reindex': | ||||
|             index_reindex() | ||||
|         elif options['command'] == 'optimize': | ||||
|             index_optimize() | ||||
|         with transaction.atomic(): | ||||
|             if options['command'] == 'reindex': | ||||
|                 index_reindex() | ||||
|             elif options['command'] == 'optimize': | ||||
|                 index_optimize() | ||||
|   | ||||
| @@ -64,6 +64,58 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): | ||||
|  | ||||
|         self.assertEqual(len(Document.objects.all()), 0) | ||||
|  | ||||
|     def test_document_fields(self): | ||||
|         c = Correspondent.objects.create(name="c", pk=41) | ||||
|         dt = DocumentType.objects.create(name="dt", pk=63) | ||||
|         tag = Tag.objects.create(name="t", pk=85) | ||||
|         doc = Document.objects.create(title="WOW", content="the content", correspondent=c, document_type=dt, checksum="123", mime_type="application/pdf") | ||||
|  | ||||
|         response = self.client.get("/api/documents/", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results_full = response.data['results'] | ||||
|         self.assertTrue("content" in results_full[0]) | ||||
|         self.assertTrue("id" in results_full[0]) | ||||
|  | ||||
|         response = self.client.get("/api/documents/?fields=id", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results = response.data['results'] | ||||
|         self.assertFalse("content" in results[0]) | ||||
|         self.assertTrue("id" in results[0]) | ||||
|         self.assertEqual(len(results[0]), 1) | ||||
|  | ||||
|         response = self.client.get("/api/documents/?fields=content", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results = response.data['results'] | ||||
|         self.assertTrue("content" in results[0]) | ||||
|         self.assertFalse("id" in results[0]) | ||||
|         self.assertEqual(len(results[0]), 1) | ||||
|  | ||||
|         response = self.client.get("/api/documents/?fields=id,content", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results = response.data['results'] | ||||
|         self.assertTrue("content" in results[0]) | ||||
|         self.assertTrue("id" in results[0]) | ||||
|         self.assertEqual(len(results[0]), 2) | ||||
|  | ||||
|         response = self.client.get("/api/documents/?fields=id,conteasdnt", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results = response.data['results'] | ||||
|         self.assertFalse("content" in results[0]) | ||||
|         self.assertTrue("id" in results[0]) | ||||
|         self.assertEqual(len(results[0]), 1) | ||||
|  | ||||
|         response = self.client.get("/api/documents/?fields=", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results = response.data['results'] | ||||
|         self.assertEqual(results_full, results) | ||||
|  | ||||
|         response = self.client.get("/api/documents/?fields=dgfhs", format='json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         results = response.data['results'] | ||||
|         self.assertEqual(len(results[0]), 0) | ||||
|  | ||||
|  | ||||
|  | ||||
|     def test_document_actions(self): | ||||
|  | ||||
|         _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) | ||||
| @@ -683,7 +735,6 @@ class TestBulkEdit(DirectoriesMixin, APITestCase): | ||||
|         args, kwargs = self.async_task.call_args | ||||
|         self.assertCountEqual(kwargs['document_ids'], [self.doc1.id, self.doc3.id]) | ||||
|  | ||||
|  | ||||
|     def test_remove_tag(self): | ||||
|         self.assertEqual(Document.objects.filter(tags__id=self.t1.id).count(), 2) | ||||
|         bulk_edit.remove_tag([self.doc1.id, self.doc3.id, self.doc4.id], self.t1.id) | ||||
| @@ -698,15 +749,103 @@ class TestBulkEdit(DirectoriesMixin, APITestCase): | ||||
|         self.assertEqual(Document.objects.count(), 3) | ||||
|         self.assertCountEqual([doc.id for doc in Document.objects.all()], [self.doc3.id, self.doc4.id, self.doc5.id]) | ||||
|  | ||||
|     def test_api(self): | ||||
|         self.assertEqual(Document.objects.count(), 5) | ||||
|     @mock.patch("documents.serialisers.bulk_edit.set_correspondent") | ||||
|     def test_api_set_correspondent(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "set_correspondent", | ||||
|             "parameters": {"correspondent": self.c1.id} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertEqual(kwargs['correspondent'], self.c1.id) | ||||
|  | ||||
|     @mock.patch("documents.serialisers.bulk_edit.set_correspondent") | ||||
|     def test_api_unset_correspondent(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "set_correspondent", | ||||
|             "parameters": {"correspondent": None} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertIsNone(kwargs['correspondent']) | ||||
|  | ||||
|     @mock.patch("documents.serialisers.bulk_edit.set_document_type") | ||||
|     def test_api_set_type(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "set_document_type", | ||||
|             "parameters": {"document_type": self.dt1.id} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertEqual(kwargs['document_type'], self.dt1.id) | ||||
|  | ||||
|     @mock.patch("documents.serialisers.bulk_edit.set_document_type") | ||||
|     def test_api_unset_type(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "set_document_type", | ||||
|             "parameters": {"document_type": None} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertIsNone(kwargs['document_type']) | ||||
|  | ||||
|     @mock.patch("documents.serialisers.bulk_edit.add_tag") | ||||
|     def test_api_add_tag(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "add_tag", | ||||
|             "parameters": {"tag": self.t1.id} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertEqual(kwargs['tag'], self.t1.id) | ||||
|  | ||||
|     @mock.patch("documents.serialisers.bulk_edit.remove_tag") | ||||
|     def test_api_remove_tag(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "remove_tag", | ||||
|             "parameters": {"tag": self.t1.id} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertEqual(kwargs['tag'], self.t1.id) | ||||
|  | ||||
|     @mock.patch("documents.serialisers.bulk_edit.delete") | ||||
|     def test_api_delete(self, m): | ||||
|         m.return_value = "OK" | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc1.id], | ||||
|             "method": "delete", | ||||
|             "parameters": {} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertEqual(Document.objects.count(), 4) | ||||
|         m.assert_called_once() | ||||
|         args, kwargs = m.call_args | ||||
|         self.assertEqual(args[0], [self.doc1.id]) | ||||
|         self.assertEqual(len(kwargs), 0) | ||||
|  | ||||
|     def test_api_invalid_doc(self): | ||||
|         self.assertEqual(Document.objects.count(), 5) | ||||
| @@ -727,3 +866,38 @@ class TestBulkEdit(DirectoriesMixin, APITestCase): | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|         self.assertEqual(Document.objects.count(), 5) | ||||
|  | ||||
|     def test_api_invalid_correspondent(self): | ||||
|         self.assertEqual(self.doc2.correspondent, self.c1) | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc2.id], | ||||
|             "method": "set_correspondent", | ||||
|             "parameters": {'correspondent': 345657} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|  | ||||
|         doc2 = Document.objects.get(id=self.doc2.id) | ||||
|         self.assertEqual(doc2.correspondent, self.c1) | ||||
|  | ||||
|     def test_api_invalid_document_type(self): | ||||
|         self.assertEqual(self.doc2.document_type, self.dt1) | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc2.id], | ||||
|             "method": "set_document_type", | ||||
|             "parameters": {'document_type': 345657} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|  | ||||
|         doc2 = Document.objects.get(id=self.doc2.id) | ||||
|         self.assertEqual(doc2.document_type, self.dt1) | ||||
|  | ||||
|     def test_api_invalid_tag(self): | ||||
|         self.assertEqual(list(self.doc2.tags.all()), [self.t1]) | ||||
|         response = self.client.post("/api/documents/bulk_edit/", json.dumps({ | ||||
|             "documents": [self.doc2.id], | ||||
|             "method": "add_tag", | ||||
|             "parameters": {'document_type': 345657} | ||||
|         }), content_type='application/json') | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|  | ||||
|         self.assertEqual(list(self.doc2.tags.all()), [self.t1]) | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import unittest | ||||
| from unittest import mock | ||||
|  | ||||
| from django.core.checks import Error | ||||
| from django.test import TestCase | ||||
|  | ||||
| from .factories import DocumentFactory | ||||
| from ..checks import changed_password_check | ||||
| from .. import document_consumer_declaration | ||||
| from ..checks import changed_password_check, parser_check | ||||
| from ..models import Document | ||||
|  | ||||
|  | ||||
| @@ -15,3 +18,13 @@ class ChecksTestCase(TestCase): | ||||
|     def test_changed_password_check_no_encryption(self): | ||||
|         DocumentFactory.create(storage_type=Document.STORAGE_TYPE_UNENCRYPTED) | ||||
|         self.assertEqual(changed_password_check(None), []) | ||||
|  | ||||
|     def test_parser_check(self): | ||||
|  | ||||
|         self.assertEqual(parser_check(None), []) | ||||
|  | ||||
|         with mock.patch('documents.checks.document_consumer_declaration.send') as m: | ||||
|             m.return_value = [] | ||||
|  | ||||
|             self.assertEqual(parser_check(None), [Error("No parsers found. This is a bug. The consumer won't be " | ||||
|                                                         "able to consume any documents without parsers.")]) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon