mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06: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,7 +19,7 @@ class Command(Renderable, BaseCommand):
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
 | 
			
		||||
        self.verbosity = options["verbosity"]
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            if options['command'] == 'reindex':
 | 
			
		||||
                index_reindex()
 | 
			
		||||
            elif options['command'] == '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