diff --git a/docs/api.md b/docs/api.md index ccbde9b22..c5f20edd1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -556,3 +556,11 @@ Initial API version. - Consumption templates were refactored to workflows and API endpoints changed as such. + +#### Version 5 + +- Added bulk deletion methods for documents and objects. + +#### Version 6 + +- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`. diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 3eee47eb8..8bd52760e 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -253,6 +253,10 @@ src/app/app.component.ts 87 + + src/app/components/admin/trash/trash.component.ts + 118 + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html 37 @@ -1480,11 +1484,11 @@ src/app/components/admin/trash/trash.component.ts - 57 + 59 src/app/components/admin/trash/trash.component.ts - 86 + 88 src/app/components/admin/users-groups/users-groups.component.html @@ -2216,11 +2220,11 @@ Confirm delete src/app/components/admin/trash/trash.component.ts - 53 + 55 src/app/components/admin/trash/trash.component.ts - 80 + 82 src/app/components/manage/management-list/management-list.component.ts @@ -2235,18 +2239,18 @@ This operation will permanently delete this document. src/app/components/admin/trash/trash.component.ts - 54 + 56 This operation cannot be undone. src/app/components/admin/trash/trash.component.ts - 55 + 57 src/app/components/admin/trash/trash.component.ts - 84 + 86 src/app/components/admin/users-groups/users-groups.component.ts @@ -2281,14 +2285,14 @@ Document deleted src/app/components/admin/trash/trash.component.ts - 64 + 66 Error deleting document src/app/components/admin/trash/trash.component.ts - 69 + 71 src/app/components/document-detail/document-detail.component.ts @@ -2299,56 +2303,56 @@ This operation will permanently delete the selected documents. src/app/components/admin/trash/trash.component.ts - 82 + 84 This operation will permanently delete all documents in the trash. src/app/components/admin/trash/trash.component.ts - 83 + 85 Document(s) deleted src/app/components/admin/trash/trash.component.ts - 94 + 96 Error deleting document(s) src/app/components/admin/trash/trash.component.ts - 101 + 103 Document restored src/app/components/admin/trash/trash.component.ts - 113 + 116 Error restoring document src/app/components/admin/trash/trash.component.ts - 117 + 126 Document(s) restored src/app/components/admin/trash/trash.component.ts - 127 + 136 Error restoring document(s) src/app/components/admin/trash/trash.component.ts - 133 + 142 @@ -5437,36 +5441,36 @@ TOTP activated successfully src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 263 + 264 Error activating TOTP src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 265 + 266 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 271 + 272 TOTP deactivated successfully src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 287 + 288 Error deactivating TOTP src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 289 + 290 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts - 294 + 295 diff --git a/src-ui/src/app/components/admin/trash/trash.component.spec.ts b/src-ui/src/app/components/admin/trash/trash.component.spec.ts index c9e797a1f..9ac89d9a5 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.spec.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.spec.ts @@ -16,6 +16,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial import { By } from '@angular/platform-browser' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { ToastService } from 'src/app/services/toast.service' +import { Router } from '@angular/router' const documentsInTrash = [ { @@ -38,6 +39,7 @@ describe('TrashComponent', () => { let trashService: TrashService let modalService: NgbModal let toastService: ToastService + let router: Router beforeEach(async () => { await TestBed.configureTestingModule({ @@ -61,6 +63,7 @@ describe('TrashComponent', () => { trashService = TestBed.inject(TrashService) modalService = TestBed.inject(NgbModal) toastService = TestBed.inject(ToastService) + router = TestBed.inject(Router) component = fixture.componentInstance fixture.detectChanges() }) @@ -161,6 +164,22 @@ describe('TrashComponent', () => { expect(restoreSpy).toHaveBeenCalledWith([1, 2]) }) + it('should offer link to restored document', () => { + let toasts + const navigateSpy = jest.spyOn(router, 'navigate') + toastService.getToasts().subscribe((allToasts) => { + toasts = [...allToasts] + }) + jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK')) + component.restore(documentsInTrash[0]) + expect(toasts.length).toEqual(1) + toasts[0].action() + expect(navigateSpy).toHaveBeenCalledWith([ + 'documents', + documentsInTrash[0].id, + ]) + }) + it('should support toggle all items in view', () => { component.documentsInTrash = documentsInTrash expect(component.selectedDocuments.size).toEqual(0) diff --git a/src-ui/src/app/components/admin/trash/trash.component.ts b/src-ui/src/app/components/admin/trash/trash.component.ts index cf807e831..9364d4cce 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.ts @@ -7,6 +7,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial import { Subject, takeUntil } from 'rxjs' import { SettingsService } from 'src/app/services/settings.service' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { Router } from '@angular/router' @Component({ selector: 'pngx-trash', @@ -26,7 +27,8 @@ export class TrashComponent implements OnDestroy { private trashService: TrashService, private toastService: ToastService, private modalService: NgbModal, - private settingsService: SettingsService + private settingsService: SettingsService, + private router: Router ) { this.reload() } @@ -110,7 +112,14 @@ export class TrashComponent implements OnDestroy { restore(document: Document) { this.trashService.restoreDocuments([document.id]).subscribe({ next: () => { - this.toastService.showInfo($localize`Document restored`) + this.toastService.show({ + content: $localize`Document restored`, + delay: 5000, + actionName: $localize`Open document`, + action: () => { + this.router.navigate(['documents', document.id]) + }, + }) this.reload() }, error: (err) => { diff --git a/src-ui/src/app/data/paperless-task.ts b/src-ui/src/app/data/paperless-task.ts index 08b30d44b..d15f006d7 100644 --- a/src-ui/src/app/data/paperless-task.ts +++ b/src-ui/src/app/data/paperless-task.ts @@ -30,4 +30,6 @@ export interface PaperlessTask extends ObjectWithId { result?: string related_document?: number + + owner?: number } diff --git a/src-ui/src/app/services/tasks.service.spec.ts b/src-ui/src/app/services/tasks.service.spec.ts index 41a374831..d746707b7 100644 --- a/src-ui/src/app/services/tasks.service.spec.ts +++ b/src-ui/src/app/services/tasks.service.spec.ts @@ -48,7 +48,7 @@ describe('TasksService', () => { it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => { tasksService.dismissTasks(new Set([1, 2, 3])) const req = httpTestingController.expectOne( - `${environment.apiBaseUrl}acknowledge_tasks/` + `${environment.apiBaseUrl}tasks/acknowledge/` ) expect(req.request.method).toEqual('POST') expect(req.request.body).toEqual({ diff --git a/src-ui/src/app/services/tasks.service.ts b/src-ui/src/app/services/tasks.service.ts index e2c064e03..c3c8f1d2b 100644 --- a/src-ui/src/app/services/tasks.service.ts +++ b/src-ui/src/app/services/tasks.service.ts @@ -64,7 +64,7 @@ export class TasksService { public dismissTasks(task_ids: Set) { this.http - .post(`${this.baseUrl}acknowledge_tasks/`, { + .post(`${this.baseUrl}tasks/acknowledge/`, { tasks: [...task_ids], }) .pipe(first()) diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 76ba37891..ba01ac9b0 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI) export const environment = { production: true, apiBaseUrl: document.baseURI + 'api/', - apiVersion: '5', + apiVersion: '6', appTitle: 'Paperless-ngx', version: '2.13.5', webSocketHost: window.location.host, diff --git a/src-ui/src/environments/environment.ts b/src-ui/src/environments/environment.ts index 18715e90f..6256f3ae3 100644 --- a/src-ui/src/environments/environment.ts +++ b/src-ui/src/environments/environment.ts @@ -5,7 +5,7 @@ export const environment = { production: false, apiBaseUrl: 'http://localhost:8000/api/', - apiVersion: '5', + apiVersion: '6', appTitle: 'Paperless-ngx', version: 'DEVELOPMENT', webSocketHost: 'localhost:8000', diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index 4d03769c8..83be5eea9 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -24,7 +24,7 @@ from documents.models import StoragePath from documents.permissions import set_permissions_for_object from documents.tasks import bulk_update_documents from documents.tasks import consume_file -from documents.tasks import update_document_archive_file +from documents.tasks import update_document_content_maybe_archive_file logger: logging.Logger = logging.getLogger("paperless.bulk_edit") @@ -191,7 +191,7 @@ def delete(doc_ids: list[int]) -> Literal["OK"]: def reprocess(doc_ids: list[int]) -> Literal["OK"]: for document_id in doc_ids: - update_document_archive_file.delay( + update_document_content_maybe_archive_file.delay( document_id=document_id, ) @@ -245,7 +245,7 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]: doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest() doc.save() rotate_tasks.append( - update_document_archive_file.s( + update_document_content_maybe_archive_file.s( document_id=doc.id, ), ) @@ -423,7 +423,7 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: if doc.page_count is not None: doc.page_count = doc.page_count - len(pages) doc.save() - update_document_archive_file.delay(document_id=doc.id) + update_document_content_maybe_archive_file.delay(document_id=doc.id) logger.info(f"Deleted pages {pages} from document {doc.id}") except Exception as e: logger.exception(f"Error deleting pages from document {doc.id}: {e}") diff --git a/src/documents/management/commands/document_archiver.py b/src/documents/management/commands/document_archiver.py index 12677dd79..1aa52117a 100644 --- a/src/documents/management/commands/document_archiver.py +++ b/src/documents/management/commands/document_archiver.py @@ -9,7 +9,7 @@ from django.core.management.base import BaseCommand from documents.management.commands.mixins import MultiProcessMixin from documents.management.commands.mixins import ProgressBarMixin from documents.models import Document -from documents.tasks import update_document_archive_file +from documents.tasks import update_document_content_maybe_archive_file logger = logging.getLogger("paperless.management.archiver") @@ -77,13 +77,13 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand): if self.process_count == 1: for doc_id in document_ids: - update_document_archive_file(doc_id) + update_document_content_maybe_archive_file(doc_id) else: # pragma: no cover with multiprocessing.Pool(self.process_count) as pool: list( tqdm.tqdm( pool.imap_unordered( - update_document_archive_file, + update_document_content_maybe_archive_file, document_ids, ), total=len(document_ids), diff --git a/src/documents/migrations/1057_paperlesstask_owner.py b/src/documents/migrations/1057_paperlesstask_owner.py new file mode 100644 index 000000000..e9f108d3a --- /dev/null +++ b/src/documents/migrations/1057_paperlesstask_owner.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.1 on 2024-11-04 21:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1056_customfieldinstance_deleted_at_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="paperlesstask", + name="owner", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="owner", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 4528d5127..05226b0e9 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -641,7 +641,7 @@ class UiSettings(models.Model): return self.user.username -class PaperlessTask(models.Model): +class PaperlessTask(ModelWithOwner): ALL_STATES = sorted(states.ALL_STATES) TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES)) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 45bf672d8..f960cac24 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1567,7 +1567,7 @@ class UiSettingsViewSerializer(serializers.ModelSerializer): return ui_settings -class TasksViewSerializer(serializers.ModelSerializer): +class TasksViewSerializer(OwnedObjectSerializer): class Meta: model = PaperlessTask depth = 1 @@ -1582,6 +1582,7 @@ class TasksViewSerializer(serializers.ModelSerializer): "result", "acknowledged", "related_document", + "owner", ) type = serializers.SerializerMethodField() diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 114654c64..cd2e3972e 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -939,9 +939,10 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs): close_old_connections() task_args = body[0] - input_doc, _ = task_args + input_doc, overrides = task_args task_file_name = input_doc.original_file.name + user_id = overrides.owner_id if overrides else None PaperlessTask.objects.create( task_id=headers["id"], @@ -952,6 +953,7 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs): date_created=timezone.now(), date_started=None, date_done=None, + owner_id=user_id, ) except Exception: # pragma: no cover # Don't let an exception in the signal handlers prevent diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 8f5ee51bc..e04cdb34e 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -206,9 +206,10 @@ def bulk_update_documents(document_ids): @shared_task -def update_document_archive_file(document_id): +def update_document_content_maybe_archive_file(document_id): """ - Re-creates the archive file of a document, including new OCR content and thumbnail + Re-creates OCR content and thumbnail for a document, and archive file if + it exists. """ document = Document.objects.get(id=document_id) @@ -234,8 +235,9 @@ def update_document_archive_file(document_id): document.get_public_filename(), ) - if parser.get_archive_path(): - with transaction.atomic(): + with transaction.atomic(): + oldDocument = Document.objects.get(pk=document.pk) + if parser.get_archive_path(): with open(parser.get_archive_path(), "rb") as f: checksum = hashlib.md5(f.read()).hexdigest() # I'm going to save first so that in case the file move @@ -246,7 +248,6 @@ def update_document_archive_file(document_id): document, archive_filename=True, ) - oldDocument = Document.objects.get(pk=document.pk) Document.objects.filter(pk=document.pk).update( archive_checksum=checksum, content=parser.get_text(), @@ -268,24 +269,41 @@ def update_document_archive_file(document_id): ], }, additional_data={ - "reason": "Update document archive file", + "reason": "Update document content", + }, + action=LogEntry.Action.UPDATE, + ) + else: + Document.objects.filter(pk=document.pk).update( + content=parser.get_text(), + ) + + if settings.AUDIT_LOG_ENABLED: + LogEntry.objects.log_create( + instance=oldDocument, + changes={ + "content": [oldDocument.content, parser.get_text()], + }, + additional_data={ + "reason": "Update document content", }, action=LogEntry.Action.UPDATE, ) - with FileLock(settings.MEDIA_LOCK): + with FileLock(settings.MEDIA_LOCK): + if parser.get_archive_path(): create_source_path_directory(document.archive_path) shutil.move(parser.get_archive_path(), document.archive_path) - shutil.move(thumbnail, document.thumbnail_path) + shutil.move(thumbnail, document.thumbnail_path) - document.refresh_from_db() - logger.info( - f"Updating index for document {document_id} ({document.archive_checksum})", - ) - with index.open_index_writer() as writer: - index.update_document(writer, document) + document.refresh_from_db() + logger.info( + f"Updating index for document {document_id} ({document.archive_checksum})", + ) + with index.open_index_writer() as writer: + index.update_document(writer, document) - clear_document_caches(document.pk) + clear_document_caches(document.pk) except Exception: logger.exception( diff --git a/src/documents/tests/test_api_tasks.py b/src/documents/tests/test_api_tasks.py index 52ffb09fe..dd5425278 100644 --- a/src/documents/tests/test_api_tasks.py +++ b/src/documents/tests/test_api_tasks.py @@ -1,6 +1,7 @@ import uuid import celery +from django.contrib.auth.models import Permission from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase @@ -11,7 +12,6 @@ from documents.tests.utils import DirectoriesMixin class TestTasks(DirectoriesMixin, APITestCase): ENDPOINT = "/api/tasks/" - ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/" def setUp(self): super().setUp() @@ -125,7 +125,7 @@ class TestTasks(DirectoriesMixin, APITestCase): self.assertEqual(len(response.data), 1) response = self.client.post( - self.ENDPOINT_ACKNOWLEDGE, + self.ENDPOINT + "acknowledge/", {"tasks": [task.id]}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -133,6 +133,52 @@ class TestTasks(DirectoriesMixin, APITestCase): response = self.client.get(self.ENDPOINT) self.assertEqual(len(response.data), 0) + def test_tasks_owner_aware(self): + """ + GIVEN: + - Existing PaperlessTasks with owner and with no owner + WHEN: + - API call is made to get tasks + THEN: + - Only tasks with no owner or request user are returned + """ + + regular_user = User.objects.create_user(username="test") + regular_user.user_permissions.add(*Permission.objects.all()) + self.client.logout() + self.client.force_authenticate(user=regular_user) + + task1 = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_one.pdf", + owner=self.user, + ) + + task2 = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_two.pdf", + ) + + task3 = PaperlessTask.objects.create( + task_id=str(uuid.uuid4()), + task_file_name="task_three.pdf", + owner=regular_user, + ) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]["task_id"], task3.task_id) + self.assertEqual(response.data[1]["task_id"], task2.task_id) + + acknowledge_response = self.client.post( + self.ENDPOINT + "acknowledge/", + {"tasks": [task1.id, task2.id, task3.id]}, + ) + self.assertEqual(acknowledge_response.status_code, status.HTTP_200_OK) + self.assertEqual(acknowledge_response.data, {"result": 2}) + def test_task_result_no_error(self): """ GIVEN: diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index c6e846a77..bb5ebf04d 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -607,7 +607,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): mock_consume_file.assert_not_called() @mock.patch("documents.tasks.bulk_update_documents.si") - @mock.patch("documents.tasks.update_document_archive_file.s") + @mock.patch("documents.tasks.update_document_content_maybe_archive_file.s") @mock.patch("celery.chord.delay") def test_rotate(self, mock_chord, mock_update_document, mock_update_documents): """ @@ -626,7 +626,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): self.assertEqual(result, "OK") @mock.patch("documents.tasks.bulk_update_documents.si") - @mock.patch("documents.tasks.update_document_archive_file.s") + @mock.patch("documents.tasks.update_document_content_maybe_archive_file.s") @mock.patch("pikepdf.Pdf.save") def test_rotate_with_error( self, @@ -654,7 +654,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): mock_update_archive_file.assert_not_called() @mock.patch("documents.tasks.bulk_update_documents.si") - @mock.patch("documents.tasks.update_document_archive_file.s") + @mock.patch("documents.tasks.update_document_content_maybe_archive_file.s") @mock.patch("celery.chord.delay") def test_rotate_non_pdf( self, @@ -680,7 +680,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): mock_chord.assert_called_once() self.assertEqual(result, "OK") - @mock.patch("documents.tasks.update_document_archive_file.delay") + @mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay") @mock.patch("pikepdf.Pdf.save") def test_delete_pages(self, mock_pdf_save, mock_update_archive_file): """ @@ -705,7 +705,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): self.doc2.refresh_from_db() self.assertEqual(self.doc2.page_count, expected_page_count) - @mock.patch("documents.tasks.update_document_archive_file.delay") + @mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay") @mock.patch("pikepdf.Pdf.save") def test_delete_pages_with_error(self, mock_pdf_save, mock_update_archive_file): """ diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 76a0a2c74..5340035e7 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -13,7 +13,7 @@ from django.test import override_settings from documents.file_handling import generate_filename from documents.models import Document -from documents.tasks import update_document_archive_file +from documents.tasks import update_document_content_maybe_archive_file from documents.tests.utils import DirectoriesMixin from documents.tests.utils import FileSystemAssertsMixin @@ -46,7 +46,7 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf"), ) - update_document_archive_file(doc.pk) + update_document_content_maybe_archive_file(doc.pk) doc = Document.objects.get(id=doc.id) @@ -63,7 +63,7 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): doc.save() shutil.copy(sample_file, doc.source_path) - update_document_archive_file(doc.pk) + update_document_content_maybe_archive_file(doc.pk) doc = Document.objects.get(id=doc.id) @@ -94,8 +94,8 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): os.path.join(self.dirs.originals_dir, "document_01.pdf"), ) - update_document_archive_file(doc2.pk) - update_document_archive_file(doc1.pk) + update_document_content_maybe_archive_file(doc2.pk) + update_document_content_maybe_archive_file(doc1.pk) doc1 = Document.objects.get(id=doc1.id) doc2 = Document.objects.get(id=doc2.id) diff --git a/src/documents/tests/test_task_signals.py b/src/documents/tests/test_task_signals.py index 4a54220e0..a025fb9dc 100644 --- a/src/documents/tests/test_task_signals.py +++ b/src/documents/tests/test_task_signals.py @@ -5,6 +5,7 @@ import celery from django.test import TestCase from documents.data_models import ConsumableDocument +from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource from documents.models import PaperlessTask from documents.signals.handlers import before_task_publish_handler @@ -48,7 +49,10 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase): source=DocumentSource.ConsumeFolder, original_file="/consume/hello-999.pdf", ), - None, + DocumentMetadataOverrides( + title="Hello world", + owner_id=1, + ), ), # kwargs {}, @@ -65,6 +69,7 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase): self.assertEqual(headers["id"], task.task_id) self.assertEqual("hello-999.pdf", task.task_file_name) self.assertEqual("documents.tasks.consume_file", task.task_name) + self.assertEqual(1, task.owner_id) self.assertEqual(celery.states.PENDING, task.status) def test_task_prerun_handler(self): diff --git a/src/documents/tests/test_tasks.py b/src/documents/tests/test_tasks.py index a1d21b532..0f9f8511f 100644 --- a/src/documents/tests/test_tasks.py +++ b/src/documents/tests/test_tasks.py @@ -1,5 +1,7 @@ import os +import shutil from datetime import timedelta +from pathlib import Path from unittest import mock from django.conf import settings @@ -184,3 +186,75 @@ class TestEmptyTrashTask(DirectoriesMixin, FileSystemAssertsMixin, TestCase): tasks.empty_trash() self.assertEqual(Document.global_objects.count(), 0) + + +class TestUpdateContent(DirectoriesMixin, TestCase): + def test_update_content_maybe_archive_file(self): + """ + GIVEN: + - Existing document with archive file + WHEN: + - Update content task is called + THEN: + - Document is reprocessed, content and checksum are updated + """ + sample1 = self.dirs.scratch_dir / "sample.pdf" + shutil.copy( + Path(__file__).parent + / "samples" + / "documents" + / "originals" + / "0000001.pdf", + sample1, + ) + sample1_archive = self.dirs.archive_dir / "sample_archive.pdf" + shutil.copy( + Path(__file__).parent + / "samples" + / "documents" + / "originals" + / "0000001.pdf", + sample1_archive, + ) + doc = Document.objects.create( + title="test", + content="my document", + checksum="wow", + archive_checksum="wow", + filename=sample1, + mime_type="application/pdf", + archive_filename=sample1_archive, + ) + + tasks.update_document_content_maybe_archive_file(doc.pk) + self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test") + self.assertNotEqual(Document.objects.get(pk=doc.pk).archive_checksum, "wow") + + def test_update_content_maybe_archive_file_no_archive(self): + """ + GIVEN: + - Existing document without archive file + WHEN: + - Update content task is called + THEN: + - Document is reprocessed, content is updated + """ + sample1 = self.dirs.scratch_dir / "sample.pdf" + shutil.copy( + Path(__file__).parent + / "samples" + / "documents" + / "originals" + / "0000001.pdf", + sample1, + ) + doc = Document.objects.create( + title="test", + content="my document", + checksum="wow", + filename=sample1, + mime_type="application/pdf", + ) + + tasks.update_document_content_maybe_archive_file(doc.pk) + self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test") diff --git a/src/documents/views.py b/src/documents/views.py index 2d0c030f4..332d5f64a 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1705,6 +1705,7 @@ class RemoteVersionView(GenericAPIView): class TasksViewSet(ReadOnlyModelViewSet): permission_classes = (IsAuthenticated, PaperlessObjectPermissions) serializer_class = TasksViewSerializer + filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,) def get_queryset(self): queryset = ( @@ -1719,19 +1720,17 @@ class TasksViewSet(ReadOnlyModelViewSet): queryset = PaperlessTask.objects.filter(task_id=task_id) return queryset - -class AcknowledgeTasksView(GenericAPIView): - permission_classes = (IsAuthenticated,) - serializer_class = AcknowledgeTasksViewSerializer - - def post(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) + @action(methods=["post"], detail=False) + def acknowledge(self, request): + serializer = AcknowledgeTasksViewSerializer(data=request.data) serializer.is_valid(raise_exception=True) - - tasks = serializer.validated_data.get("tasks") + task_ids = serializer.validated_data.get("tasks") try: - result = PaperlessTask.objects.filter(id__in=tasks).update( + tasks = PaperlessTask.objects.filter(id__in=task_ids) + if request.user is not None and not request.user.is_superuser: + tasks = tasks.filter(owner=request.user) | tasks.filter(owner=None) + result = tasks.update( acknowledged=True, ) return Response({"result": result}) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index b57606380..1891db0e3 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -334,7 +334,7 @@ REST_FRAMEWORK = { "DEFAULT_VERSION": "1", # Make sure these are ordered and that the most recent version appears # last - "ALLOWED_VERSIONS": ["1", "2", "3", "4", "5"], + "ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6"], "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 2ebd7e739..5b7327b8d 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -18,7 +18,6 @@ from django.views.static import serve from rest_framework.authtoken import views from rest_framework.routers import DefaultRouter -from documents.views import AcknowledgeTasksView from documents.views import BulkDownloadView from documents.views import BulkEditObjectsView from documents.views import BulkEditView @@ -132,11 +131,6 @@ urlpatterns = [ name="remoteversion", ), re_path("^ui_settings/", UiSettingsView.as_view(), name="ui_settings"), - re_path( - "^acknowledge_tasks/", - AcknowledgeTasksView.as_view(), - name="acknowledge_tasks", - ), re_path( "^mail_accounts/test/", MailAccountTestView.as_view(),