mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Enhancement: support owner permissions for file tasks (#8195)
This commit is contained in:
parent
9c1561adfb
commit
8bfe68743d
@ -556,3 +556,11 @@ Initial API version.
|
|||||||
|
|
||||||
- Consumption templates were refactored to workflows and API endpoints
|
- Consumption templates were refactored to workflows and API endpoints
|
||||||
changed as such.
|
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/`.
|
||||||
|
@ -30,4 +30,6 @@ export interface PaperlessTask extends ObjectWithId {
|
|||||||
result?: string
|
result?: string
|
||||||
|
|
||||||
related_document?: number
|
related_document?: number
|
||||||
|
|
||||||
|
owner?: number
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ describe('TasksService', () => {
|
|||||||
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
||||||
tasksService.dismissTasks(new Set([1, 2, 3]))
|
tasksService.dismissTasks(new Set([1, 2, 3]))
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}acknowledge_tasks/`
|
`${environment.apiBaseUrl}tasks/acknowledge/`
|
||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('POST')
|
expect(req.request.method).toEqual('POST')
|
||||||
expect(req.request.body).toEqual({
|
expect(req.request.body).toEqual({
|
||||||
|
@ -64,7 +64,7 @@ export class TasksService {
|
|||||||
|
|
||||||
public dismissTasks(task_ids: Set<number>) {
|
public dismissTasks(task_ids: Set<number>) {
|
||||||
this.http
|
this.http
|
||||||
.post(`${this.baseUrl}acknowledge_tasks/`, {
|
.post(`${this.baseUrl}tasks/acknowledge/`, {
|
||||||
tasks: [...task_ids],
|
tasks: [...task_ids],
|
||||||
})
|
})
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
|
@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI)
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
apiBaseUrl: document.baseURI + 'api/',
|
apiBaseUrl: document.baseURI + 'api/',
|
||||||
apiVersion: '5',
|
apiVersion: '6',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
version: '2.13.5',
|
version: '2.13.5',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBaseUrl: 'http://localhost:8000/api/',
|
apiBaseUrl: 'http://localhost:8000/api/',
|
||||||
apiVersion: '5',
|
apiVersion: '6',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
version: 'DEVELOPMENT',
|
version: 'DEVELOPMENT',
|
||||||
webSocketHost: 'localhost:8000',
|
webSocketHost: 'localhost:8000',
|
||||||
|
28
src/documents/migrations/1057_paperlesstask_owner.py
Normal file
28
src/documents/migrations/1057_paperlesstask_owner.py
Normal file
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -641,7 +641,7 @@ class UiSettings(models.Model):
|
|||||||
return self.user.username
|
return self.user.username
|
||||||
|
|
||||||
|
|
||||||
class PaperlessTask(models.Model):
|
class PaperlessTask(ModelWithOwner):
|
||||||
ALL_STATES = sorted(states.ALL_STATES)
|
ALL_STATES = sorted(states.ALL_STATES)
|
||||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||||
|
|
||||||
|
@ -1567,7 +1567,7 @@ class UiSettingsViewSerializer(serializers.ModelSerializer):
|
|||||||
return ui_settings
|
return ui_settings
|
||||||
|
|
||||||
|
|
||||||
class TasksViewSerializer(serializers.ModelSerializer):
|
class TasksViewSerializer(OwnedObjectSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PaperlessTask
|
model = PaperlessTask
|
||||||
depth = 1
|
depth = 1
|
||||||
@ -1582,6 +1582,7 @@ class TasksViewSerializer(serializers.ModelSerializer):
|
|||||||
"result",
|
"result",
|
||||||
"acknowledged",
|
"acknowledged",
|
||||||
"related_document",
|
"related_document",
|
||||||
|
"owner",
|
||||||
)
|
)
|
||||||
|
|
||||||
type = serializers.SerializerMethodField()
|
type = serializers.SerializerMethodField()
|
||||||
|
@ -939,9 +939,10 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs):
|
|||||||
close_old_connections()
|
close_old_connections()
|
||||||
|
|
||||||
task_args = body[0]
|
task_args = body[0]
|
||||||
input_doc, _ = task_args
|
input_doc, overrides = task_args
|
||||||
|
|
||||||
task_file_name = input_doc.original_file.name
|
task_file_name = input_doc.original_file.name
|
||||||
|
user_id = overrides.owner_id if overrides else None
|
||||||
|
|
||||||
PaperlessTask.objects.create(
|
PaperlessTask.objects.create(
|
||||||
task_id=headers["id"],
|
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_created=timezone.now(),
|
||||||
date_started=None,
|
date_started=None,
|
||||||
date_done=None,
|
date_done=None,
|
||||||
|
owner_id=user_id,
|
||||||
)
|
)
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# Don't let an exception in the signal handlers prevent
|
# Don't let an exception in the signal handlers prevent
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import celery
|
import celery
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
@ -11,7 +12,6 @@ from documents.tests.utils import DirectoriesMixin
|
|||||||
|
|
||||||
class TestTasks(DirectoriesMixin, APITestCase):
|
class TestTasks(DirectoriesMixin, APITestCase):
|
||||||
ENDPOINT = "/api/tasks/"
|
ENDPOINT = "/api/tasks/"
|
||||||
ENDPOINT_ACKNOWLEDGE = "/api/acknowledge_tasks/"
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@ -125,7 +125,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(len(response.data), 1)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.ENDPOINT_ACKNOWLEDGE,
|
self.ENDPOINT + "acknowledge/",
|
||||||
{"tasks": [task.id]},
|
{"tasks": [task.id]},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@ -133,6 +133,52 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
|||||||
response = self.client.get(self.ENDPOINT)
|
response = self.client.get(self.ENDPOINT)
|
||||||
self.assertEqual(len(response.data), 0)
|
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):
|
def test_task_result_no_error(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
@ -5,6 +5,7 @@ import celery
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.signals.handlers import before_task_publish_handler
|
from documents.signals.handlers import before_task_publish_handler
|
||||||
@ -48,7 +49,10 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
|
|||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
original_file="/consume/hello-999.pdf",
|
original_file="/consume/hello-999.pdf",
|
||||||
),
|
),
|
||||||
None,
|
DocumentMetadataOverrides(
|
||||||
|
title="Hello world",
|
||||||
|
owner_id=1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
# kwargs
|
# kwargs
|
||||||
{},
|
{},
|
||||||
@ -65,6 +69,7 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
|
|||||||
self.assertEqual(headers["id"], task.task_id)
|
self.assertEqual(headers["id"], task.task_id)
|
||||||
self.assertEqual("hello-999.pdf", task.task_file_name)
|
self.assertEqual("hello-999.pdf", task.task_file_name)
|
||||||
self.assertEqual("documents.tasks.consume_file", task.task_name)
|
self.assertEqual("documents.tasks.consume_file", task.task_name)
|
||||||
|
self.assertEqual(1, task.owner_id)
|
||||||
self.assertEqual(celery.states.PENDING, task.status)
|
self.assertEqual(celery.states.PENDING, task.status)
|
||||||
|
|
||||||
def test_task_prerun_handler(self):
|
def test_task_prerun_handler(self):
|
||||||
|
@ -1705,6 +1705,7 @@ class RemoteVersionView(GenericAPIView):
|
|||||||
class TasksViewSet(ReadOnlyModelViewSet):
|
class TasksViewSet(ReadOnlyModelViewSet):
|
||||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
serializer_class = TasksViewSerializer
|
serializer_class = TasksViewSerializer
|
||||||
|
filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = (
|
queryset = (
|
||||||
@ -1719,19 +1720,17 @@ class TasksViewSet(ReadOnlyModelViewSet):
|
|||||||
queryset = PaperlessTask.objects.filter(task_id=task_id)
|
queryset = PaperlessTask.objects.filter(task_id=task_id)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@action(methods=["post"], detail=False)
|
||||||
class AcknowledgeTasksView(GenericAPIView):
|
def acknowledge(self, request):
|
||||||
permission_classes = (IsAuthenticated,)
|
serializer = AcknowledgeTasksViewSerializer(data=request.data)
|
||||||
serializer_class = AcknowledgeTasksViewSerializer
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
task_ids = serializer.validated_data.get("tasks")
|
||||||
tasks = serializer.validated_data.get("tasks")
|
|
||||||
|
|
||||||
try:
|
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,
|
acknowledged=True,
|
||||||
)
|
)
|
||||||
return Response({"result": result})
|
return Response({"result": result})
|
||||||
|
@ -333,7 +333,7 @@ REST_FRAMEWORK = {
|
|||||||
"DEFAULT_VERSION": "1",
|
"DEFAULT_VERSION": "1",
|
||||||
# Make sure these are ordered and that the most recent version appears
|
# Make sure these are ordered and that the most recent version appears
|
||||||
# last
|
# last
|
||||||
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5"],
|
"ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6"],
|
||||||
}
|
}
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
@ -18,7 +18,6 @@ from django.views.static import serve
|
|||||||
from rest_framework.authtoken import views
|
from rest_framework.authtoken import views
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from documents.views import AcknowledgeTasksView
|
|
||||||
from documents.views import BulkDownloadView
|
from documents.views import BulkDownloadView
|
||||||
from documents.views import BulkEditObjectsView
|
from documents.views import BulkEditObjectsView
|
||||||
from documents.views import BulkEditView
|
from documents.views import BulkEditView
|
||||||
@ -132,11 +131,6 @@ urlpatterns = [
|
|||||||
name="remoteversion",
|
name="remoteversion",
|
||||||
),
|
),
|
||||||
re_path("^ui_settings/", UiSettingsView.as_view(), name="ui_settings"),
|
re_path("^ui_settings/", UiSettingsView.as_view(), name="ui_settings"),
|
||||||
re_path(
|
|
||||||
"^acknowledge_tasks/",
|
|
||||||
AcknowledgeTasksView.as_view(),
|
|
||||||
name="acknowledge_tasks",
|
|
||||||
),
|
|
||||||
re_path(
|
re_path(
|
||||||
"^mail_accounts/test/",
|
"^mail_accounts/test/",
|
||||||
MailAccountTestView.as_view(),
|
MailAccountTestView.as_view(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user