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 7e0e8adbf..b41479780 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 dffffb618..9b1c1d8e3 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -940,9 +940,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"],
@@ -953,6 +954,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 e5f31800f..1a495de09 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -333,7 +333,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"],
}
if DEBUG:
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(),