]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: support owner permissions for file tasks (#8195)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 20 Nov 2024 20:25:53 +0000 (12:25 -0800)
committerGitHub <noreply@github.com>
Wed, 20 Nov 2024 20:25:53 +0000 (20:25 +0000)
15 files changed:
docs/api.md
src-ui/src/app/data/paperless-task.ts
src-ui/src/app/services/tasks.service.spec.ts
src-ui/src/app/services/tasks.service.ts
src-ui/src/environments/environment.prod.ts
src-ui/src/environments/environment.ts
src/documents/migrations/1057_paperlesstask_owner.py [new file with mode: 0644]
src/documents/models.py
src/documents/serialisers.py
src/documents/signals/handlers.py
src/documents/tests/test_api_tasks.py
src/documents/tests/test_task_signals.py
src/documents/views.py
src/paperless/settings.py
src/paperless/urls.py

index ccbde9b22fe714f9ed53b73d9ab61c1c9fb9bce5..c5f20edd179e782c2394f8dfece7ba7e75a8c905 100644 (file)
@@ -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/`.
index 08b30d44baef9362f44ae85463b71dd2160c48fb..d15f006d78fd625d0650d23cb5f32b4887a4f3ec 100644 (file)
@@ -30,4 +30,6 @@ export interface PaperlessTask extends ObjectWithId {
   result?: string
 
   related_document?: number
+
+  owner?: number
 }
index 41a37483175f7ed6c40528091fb4ab5d9390ccd0..d746707b7397e61f706687cc6bfa9a571d55c29d 100644 (file)
@@ -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({
index e2c064e03a373fe61296a831c5c020f524db5064..c3c8f1d2b8ee94630bd25fe5c4265b5ec9c45ed0 100644 (file)
@@ -64,7 +64,7 @@ export class TasksService {
 
   public dismissTasks(task_ids: Set<number>) {
     this.http
-      .post(`${this.baseUrl}acknowledge_tasks/`, {
+      .post(`${this.baseUrl}tasks/acknowledge/`, {
         tasks: [...task_ids],
       })
       .pipe(first())
index 76ba37891090d3f27dbd498d35c64e7d3a183a10..ba01ac9b0486819562e894d71480143255691926 100644 (file)
@@ -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,
index 18715e90f8fa5b594f6df374e9c377cc6605da99..6256f3ae37d97616281f66500513b009c7cf57dc 100644 (file)
@@ -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/migrations/1057_paperlesstask_owner.py b/src/documents/migrations/1057_paperlesstask_owner.py
new file mode 100644 (file)
index 0000000..e9f108d
--- /dev/null
@@ -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",
+            ),
+        ),
+    ]
index 4528d51279a4723d31928658ed7ce6aa03f90bb7..05226b0e91d1073c8c3276bd0acb2e52e716fa2b 100644 (file)
@@ -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))
 
index 45bf672d83b4dad2ad351013b0f0858884b1df3e..f960cac242721eb55e31b804c010307171ccbb6f 100644 (file)
@@ -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()
index 114654c647742bfd4d299f61c6112edb476a626d..cd2e3972e8455e69be3e20f0d05c6932cfa04507 100644 (file)
@@ -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
index 52ffb09fe9bd3234926807e92842bb6db310c461..dd5425278b1b5079446774263fbc7e7fed9034e1 100644 (file)
@@ -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:
index 4a54220e0166ee1a14d5e132be462cda0d3b9c81..a025fb9dc053e25c269e8a4bee3dceda441fdf3b 100644 (file)
@@ -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):
index 2d0c030f4f8d6411c8297505296ac9c97a0da2f0..332d5f64aaf44f8dab24c008d13b9a18cf1b196f 100644 (file)
@@ -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})
index e5f31800f7693f035b2571b470cfbcd8ba27e151..1a495de091d72221cb6405052385969c91f0f115 100644 (file)
@@ -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:
index 2ebd7e739473076a2e346ddce593219ccb12c0fb..5b7327b8dac568f0522ecdd6ec52e77604860e5c 100644 (file)
@@ -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(),