diff --git a/src-ui/src/app/components/common/input/select/select.component.html b/src-ui/src/app/components/common/input/select/select.component.html
index d33dae425..780dc5686 100644
--- a/src-ui/src/app/components/common/input/select/select.component.html
+++ b/src-ui/src/app/components/common/input/select/select.component.html
@@ -5,6 +5,7 @@
[disabled]="disabled"
[style.color]="textColor"
[style.background]="backgroundColor"
+ [clearable]="allowNull"
(change)="onChange(value)"
(blur)="onTouched()">
{{i.name}}
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html
index 228264378..ae3fb0c0a 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.html
+++ b/src-ui/src/app/components/document-detail/document-detail.component.html
@@ -67,9 +67,9 @@
formControlName='archive_serial_number'>
-
-
diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts
index 91fd0cd40..236bdeeaa 100644
--- a/src-ui/src/app/components/document-list/document-list.component.ts
+++ b/src-ui/src/app/components/document-list/document-list.component.ts
@@ -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 {
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
})
)
}
diff --git a/src-ui/src/app/services/open-documents.service.ts b/src-ui/src/app/services/open-documents.service.ts
index e37f5db8c..c91031f83 100644
--- a/src-ui/src/app/services/open-documents.service.ts
+++ b/src-ui/src/app/services/open-documents.service.ts
@@ -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
}
diff --git a/src/documents/checks.py b/src/documents/checks.py
index b6da5bfc9..ba55b1397 100644
--- a/src/documents/checks.py
+++ b/src/documents/checks.py
@@ -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 []
diff --git a/src/documents/management/commands/document_index.py b/src/documents/management/commands/document_index.py
index 7dfdbaa42..08e20e1d2 100644
--- a/src/documents/management/commands/document_index.py
+++ b/src/documents/management/commands/document_index.py
@@ -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()
diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py
index 5d2e6a3c5..bce2a433d 100644
--- a/src/documents/tests/test_api.py
+++ b/src/documents/tests/test_api.py
@@ -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])
diff --git a/src/documents/tests/test_checks.py b/src/documents/tests/test_checks.py
index 1027c11a0..ee4fbe8d1 100644
--- a/src/documents/tests/test_checks.py
+++ b/src/documents/tests/test_checks.py
@@ -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.")])