diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index 31e600b7d..6d03ec024 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -496,7 +496,7 @@ export class DocumentDetailComponent
this.toastService.showError(
$localize`Error saving document` +
': ' +
- (error.message ?? error.toString())
+ (error.error?.detail ?? error.message ?? JSON.stringify(error))
)
}
},
@@ -541,7 +541,7 @@ export class DocumentDetailComponent
this.toastService.showError(
$localize`Error saving document` +
': ' +
- (error.message ?? error.toString())
+ (error.error?.detail ?? error.message ?? JSON.stringify(error))
)
},
})
@@ -573,6 +573,10 @@ export class DocumentDetailComponent
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Delete document`
+ this.subscribeModalDelete(modal) // so can be re-subscribed if error
+ }
+
+ subscribeModalDelete(modal) {
modal.componentInstance.confirmClicked
.pipe(
switchMap(() => {
@@ -581,18 +585,21 @@ export class DocumentDetailComponent
})
)
.pipe(takeUntil(this.unsubscribeNotifier))
- .subscribe(
- () => {
+ .subscribe({
+ next: () => {
modal.close()
this.close()
},
- (error) => {
+ error: (error) => {
this.toastService.showError(
- $localize`Error deleting document: ${JSON.stringify(error)}`
+ $localize`Error deleting document: ${
+ error.error?.detail ?? error.message ?? JSON.stringify(error)
+ }`
)
modal.componentInstance.buttonsEnabled = true
- }
- )
+ this.subscribeModalDelete(modal)
+ },
+ })
}
moreLike() {
@@ -681,12 +688,21 @@ export class DocumentDetailComponent
}
}
+ get showPermissions(): boolean {
+ return (
+ this.permissionsService.currentUserCan(
+ PermissionAction.View,
+ PermissionType.User
+ ) && this.userIsOwner
+ )
+ }
+
get notesEnabled(): boolean {
return (
this.settings.get(SETTINGS_KEYS.NOTES_ENABLED) &&
this.permissionsService.currentUserCan(
PermissionAction.View,
- PermissionType.Document
+ PermissionType.Note
)
)
}
diff --git a/src-ui/src/app/guards/permissions.guard.ts b/src-ui/src/app/guards/permissions.guard.ts
index 916408fe2..7c9b7287d 100644
--- a/src-ui/src/app/guards/permissions.guard.ts
+++ b/src-ui/src/app/guards/permissions.guard.ts
@@ -8,13 +8,15 @@ import {
import { Injectable } from '@angular/core'
import { PermissionsService } from '../services/permissions.service'
import { ToastService } from '../services/toast.service'
+import { TourService } from 'ngx-ui-tour-ng-bootstrap'
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(
private permissionsService: PermissionsService,
private router: Router,
- private toastService: ToastService
+ private toastService: ToastService,
+ private tourService: TourService
) {}
canActivate(
@@ -27,9 +29,12 @@ export class PermissionsGuard implements CanActivate {
route.data.requiredPermission.type
)
) {
- this.toastService.showError(
- $localize`You don't have permissions to do that`
- )
+ // Check if tour is running 1 = TourState.ON
+ if (this.tourService.getStatus() !== 1) {
+ this.toastService.showError(
+ $localize`You don't have permissions to do that`
+ )
+ }
return this.router.parseUrl('/dashboard')
} else {
return true
diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts
index f779851c0..46e74923c 100644
--- a/src-ui/src/environments/environment.prod.ts
+++ b/src-ui/src/environments/environment.prod.ts
@@ -5,7 +5,7 @@ export const environment = {
apiBaseUrl: document.baseURI + 'api/',
apiVersion: '2',
appTitle: 'Paperless-ngx',
- version: '1.14.0-beta.rc1',
+ version: '1.14.0',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',
diff --git a/src/documents/matching.py b/src/documents/matching.py
index 63534ffe3..ad80ee0ad 100644
--- a/src/documents/matching.py
+++ b/src/documents/matching.py
@@ -6,6 +6,7 @@ from documents.models import DocumentType
from documents.models import MatchingModel
from documents.models import StoragePath
from documents.models import Tag
+from documents.permissions import get_objects_for_user_owner_aware
logger = logging.getLogger("paperless.matching")
@@ -19,40 +20,64 @@ def log_reason(matching_model, document, reason):
)
-def match_correspondents(document, classifier):
+def match_correspondents(document, classifier, user=None):
pred_id = classifier.predict_correspondent(document.content) if classifier else None
- correspondents = Correspondent.objects.all()
+ if user is not None:
+ correspondents = get_objects_for_user_owner_aware(
+ user,
+ "documents.view_correspondent",
+ Correspondent,
+ )
+ else:
+ correspondents = Correspondent.objects.all()
return list(
filter(lambda o: matches(o, document) or o.pk == pred_id, correspondents),
)
-def match_document_types(document, classifier):
+def match_document_types(document, classifier, user=None):
pred_id = classifier.predict_document_type(document.content) if classifier else None
- document_types = DocumentType.objects.all()
+ if user is not None:
+ document_types = get_objects_for_user_owner_aware(
+ user,
+ "documents.view_documenttype",
+ DocumentType,
+ )
+ else:
+ document_types = DocumentType.objects.all()
return list(
filter(lambda o: matches(o, document) or o.pk == pred_id, document_types),
)
-def match_tags(document, classifier):
+def match_tags(document, classifier, user=None):
predicted_tag_ids = classifier.predict_tags(document.content) if classifier else []
- tags = Tag.objects.all()
+ if user is not None:
+ tags = get_objects_for_user_owner_aware(user, "documents.view_tag", Tag)
+ else:
+ tags = Tag.objects.all()
return list(
filter(lambda o: matches(o, document) or o.pk in predicted_tag_ids, tags),
)
-def match_storage_paths(document, classifier):
+def match_storage_paths(document, classifier, user=None):
pred_id = classifier.predict_storage_path(document.content) if classifier else None
- storage_paths = StoragePath.objects.all()
+ if user is not None:
+ storage_paths = get_objects_for_user_owner_aware(
+ user,
+ "documents.view_storagepath",
+ StoragePath,
+ )
+ else:
+ storage_paths = StoragePath.objects.all()
return list(
filter(
diff --git a/src/documents/permissions.py b/src/documents/permissions.py
index 4af0ebae5..d4114e488 100644
--- a/src/documents/permissions.py
+++ b/src/documents/permissions.py
@@ -4,6 +4,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm
+from guardian.shortcuts import get_objects_for_user
from guardian.shortcuts import get_users_with_perms
from guardian.shortcuts import remove_perm
from rest_framework.permissions import BasePermission
@@ -101,3 +102,15 @@ def set_permissions_for_object(permissions, object):
group,
object,
)
+
+
+def get_objects_for_user_owner_aware(user, perms, Model):
+ objects_owned = Model.objects.filter(owner=user)
+ objects_unowned = Model.objects.filter(owner__isnull=True)
+ objects_with_perms = get_objects_for_user(
+ user=user,
+ perms=perms,
+ klass=Model,
+ accept_global_perms=False,
+ )
+ return objects_owned | objects_unowned | objects_with_perms
diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
index 271fb4597..a7f75c489 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -4,6 +4,7 @@ import shutil
from celery import states
from celery.signals import before_task_publish
+from celery.signals import task_failure
from celery.signals import task_postrun
from celery.signals import task_prerun
from django.conf import settings
@@ -591,3 +592,29 @@ def task_postrun_handler(
# Don't let an exception in the signal handlers prevent
# a document from being consumed.
logger.exception("Updating PaperlessTask failed")
+
+
+@task_failure.connect
+def task_failure_handler(
+ sender=None,
+ task_id=None,
+ exception=None,
+ args=None,
+ traceback=None,
+ **kwargs,
+):
+ """
+ Updates the result of a failed PaperlessTask.
+
+ https://docs.celeryq.dev/en/stable/userguide/signals.html#task-failure
+ """
+ try:
+ task_instance = PaperlessTask.objects.filter(task_id=task_id).first()
+
+ if task_instance is not None and task_instance.result is None:
+ task_instance.status = states.FAILURE
+ task_instance.result = traceback
+ task_instance.date_done = timezone.now()
+ task_instance.save()
+ except Exception: # pragma: no cover
+ logger.exception("Updating PaperlessTask failed")
diff --git a/src/documents/tests/test_task_signals.py b/src/documents/tests/test_task_signals.py
index a6befc25e..d63df0b3c 100644
--- a/src/documents/tests/test_task_signals.py
+++ b/src/documents/tests/test_task_signals.py
@@ -9,6 +9,7 @@ from documents.models import PaperlessTask
from documents.signals.handlers import before_task_publish_handler
from documents.signals.handlers import task_postrun_handler
from documents.signals.handlers import task_prerun_handler
+from documents.signals.handlers import task_failure_handler
from documents.tests.test_consumer import fake_magic_from_file
from documents.tests.utils import DirectoriesMixin
@@ -146,3 +147,44 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
task = PaperlessTask.objects.get()
self.assertEqual(celery.states.SUCCESS, task.status)
+
+ def test_task_failure_handler(self):
+ """
+ GIVEN:
+ - A celery task is started via the consume folder
+ WHEN:
+ - Task failed execution
+ THEN:
+ - The task is marked as failed
+ """
+ headers = {
+ "id": str(uuid.uuid4()),
+ "task": "documents.tasks.consume_file",
+ }
+ body = (
+ # args
+ (
+ ConsumableDocument(
+ source=DocumentSource.ConsumeFolder,
+ original_file="/consume/hello-9.pdf",
+ ),
+ None,
+ ),
+ # kwargs
+ {},
+ # celery stuff
+ {"callbacks": None, "errbacks": None, "chain": None, "chord": None},
+ )
+ self.util_call_before_task_publish_handler(
+ headers_to_use=headers,
+ body_to_use=body,
+ )
+
+ task_failure_handler(
+ task_id=headers["id"],
+ exception="Example failure",
+ )
+
+ task = PaperlessTask.objects.get()
+
+ self.assertEqual(celery.states.FAILURE, task.status)
diff --git a/src/documents/views.py b/src/documents/views.py
index 597555be9..1edbdccc3 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -401,12 +401,16 @@ class DocumentViewSet(
return Response(
{
- "correspondents": [c.id for c in match_correspondents(doc, classifier)],
- "tags": [t.id for t in match_tags(doc, classifier)],
- "document_types": [
- dt.id for dt in match_document_types(doc, classifier)
+ "correspondents": [
+ c.id for c in match_correspondents(doc, classifier, request.user)
+ ],
+ "tags": [t.id for t in match_tags(doc, classifier, request.user)],
+ "document_types": [
+ dt.id for dt in match_document_types(doc, classifier, request.user)
+ ],
+ "storage_paths": [
+ dt.id for dt in match_storage_paths(doc, classifier, request.user)
],
- "storage_paths": [dt.id for dt in match_storage_paths(doc, classifier)],
"dates": [
date.strftime("%Y-%m-%d") for date in dates if date is not None
],
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index 49f4aa6ea..77a8cc4e4 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -431,6 +431,14 @@ if _paperless_url:
# For use with trusted proxies
TRUSTED_PROXIES = __get_list("PAPERLESS_TRUSTED_PROXIES")
+USE_X_FORWARDED_HOST = __get_boolean("PAPERLESS_USE_X_FORWARD_HOST", "false")
+USE_X_FORWARDED_PORT = __get_boolean("PAPERLESS_USE_X_FORWARD_PORT", "false")
+SECURE_PROXY_SSL_HEADER = (
+ tuple(json.loads(os.environ["PAPERLESS_PROXY_SSL_HEADER"]))
+ if "PAPERLESS_PROXY_SSL_HEADER" in os.environ
+ else None
+)
+
# The secret key has a default that should be fine so long as you're hosting
# Paperless on a closed network. However, if you're putting this anywhere
# public, you should change the key to something unique and verbose.
diff --git a/src/paperless_mail/migrations/0021_alter_mailaccount_password.py b/src/paperless_mail/migrations/0021_alter_mailaccount_password.py
new file mode 100644
index 000000000..2c0f68065
--- /dev/null
+++ b/src/paperless_mail/migrations/0021_alter_mailaccount_password.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.7 on 2023-04-20 15:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("paperless_mail", "0020_mailaccount_is_token"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="mailaccount",
+ name="password",
+ field=models.CharField(max_length=2048, verbose_name="password"),
+ ),
+ ]
diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py
index c0dc1916a..a9165b248 100644
--- a/src/paperless_mail/models.py
+++ b/src/paperless_mail/models.py
@@ -36,7 +36,7 @@ class MailAccount(document_models.ModelWithOwner):
username = models.CharField(_("username"), max_length=256)
- password = models.CharField(_("password"), max_length=256)
+ password = models.CharField(_("password"), max_length=2048)
is_token = models.BooleanField(_("Is token authentication"), default=False)