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.")])