Merge branch 'dev' into feature-better-bs-icons

This commit is contained in:
shamoon 2024-01-15 15:47:43 -08:00
commit e5fa70b2b0
18 changed files with 909 additions and 742 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,10 @@
<pngx-page-header title="Application Configuration" subTitle="Global Paperless-ngx configuration options" i18n-title i18n-subTitle></pngx-page-header>
<pngx-page-header
title="Application Configuration"
i18n-title
info="Global app configuration options which apply to <strong>every</strong> user of this install of Paperless-ngx. Options can also be set using environment variables or the configuration file but the value here will always take precedence."
i18n-info
infoLink="configuration">
</pngx-page-header>
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">

View File

@ -1,4 +1,8 @@
<pngx-page-header title="Logs" i18n-title>
<pngx-page-header
title="Logs"
i18n-title
info="Review the log files for the application and for email checking."
i18n-info>
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>

View File

@ -1,4 +1,9 @@
<pngx-page-header title="Settings" i18n-title>
<pngx-page-header
title="Settings"
i18n-title
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container>

View File

@ -1,10 +1,19 @@
<pngx-page-header title="File Tasks" i18n-title>
<pngx-page-header
title="File Tasks"
i18n-title
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
i18n-info
>
<div class="btn-toolbar col col-md-auto align-items-center">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<i-bs name="check2-all"></i-bs>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container>
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
</svg>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container>
</button>
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
@ -71,18 +80,24 @@
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<i-bs class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16" name="info-circle"></i-bs>
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
</button>
</td>
<td scope="row">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check"></i-bs>&nbsp;<ng-container i18n>Dismiss</ng-container>
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<i-bs name="file-text"></i-bs>&nbsp;<ng-container i18n>Open Document</ng-container>
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
@ -100,7 +115,12 @@
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
<div class="pb-2 pb-sm-0">
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
@if (selectedTasks.size > 0) {
<ng-container i18n>&nbsp;({{selectedTasks.size}} selected)</ng-container>
}
</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>

View File

@ -1,4 +1,10 @@
<pngx-page-header title="Users & Groups" i18n-title>
<pngx-page-header
title="Users & Groups"
i18n-title
info="Create, delete and edit users and groups."
i18n-info
infoLink="usage/#users-and-groups"
>
</pngx-page-header>
@if (users) {

View File

@ -5,6 +5,22 @@
@if (subTitle) {
<span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
}
@if (info) {
<button class="btn btn-sm btn-link text-muted me-auto p-0 p-md-2" title="What's this?" i18n-title type="button" [ngbPopover]="infoPopover" [autoClose]="true">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</svg>
</button>
<ng-template #infoPopover>
<p [class.mb-0]="!infoLink" [innerHTML]="info"></p>
@if (infoLink) {
<a href="https://docs.paperless-ngx.com/{{infoLink}}" target="_blank" referrerpolicy="noopener noreferrer" i18n>Read more</a>
<svg class="sidebaricon-sm text-muted ms-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up-right"/>
</svg>
}
</ng-template>
}
</h3>
</div>
<div class="btn-toolbar col col-md-auto">

View File

@ -24,4 +24,10 @@ export class PageHeaderComponent {
@Input()
subTitle: string = ''
@Input()
info: string
@Input()
infoLink: string
}

View File

@ -1,4 +1,10 @@
<pngx-page-header title="Custom Fields" i18n-title>
<pngx-page-header
title="Custom Fields"
i18n-title
info="Customize the data fields that can be attached to documents."
i18n-info
infoLink="usage/#custom-fields"
>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Field</ng-container>
</button>

View File

@ -90,7 +90,7 @@ describe('CustomFieldsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const createButton = fixture.debugElement.queryAll(By.css('button'))[0]
const createButton = fixture.debugElement.queryAll(By.css('button'))[1]
createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@ -114,7 +114,7 @@ describe('CustomFieldsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const editButton = fixture.debugElement.queryAll(By.css('button'))[1]
const editButton = fixture.debugElement.queryAll(By.css('button'))[2]
editButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@ -139,7 +139,7 @@ describe('CustomFieldsComponent', () => {
const deleteSpy = jest.spyOn(customFieldsService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[3]
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()

View File

@ -1,4 +1,10 @@
<pngx-page-header title="Mail Settings" i18n-title>
<pngx-page-header
title="Mail Settings"
i18n-title
info="Manage e-mail accounts and rules for automatically importing documents."
i18n-info
infoLink="usage/#usage-email"
>
</pngx-page-header>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }">

View File

@ -1,4 +1,10 @@
<pngx-page-header title="Workflows" i18n-title>
<pngx-page-header
title="Workflows"
i18n-title
info="Use workflows to customize the behavior of Paperless-ngx when events 'trigger' a workflow."
i18n-info
infoLink="usage/#workflows"
>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editWorkflow()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Workflow</ng-container>
</button>

View File

@ -125,7 +125,7 @@ describe('WorkflowsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const createButton = fixture.debugElement.queryAll(By.css('button'))[0]
const createButton = fixture.debugElement.queryAll(By.css('button'))[1]
createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@ -149,7 +149,7 @@ describe('WorkflowsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const editButton = fixture.debugElement.queryAll(By.css('button'))[1]
const editButton = fixture.debugElement.queryAll(By.css('button'))[2]
editButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
@ -174,7 +174,7 @@ describe('WorkflowsComponent', () => {
const deleteSpy = jest.spyOn(workflowService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[3]
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()

View File

@ -10,6 +10,7 @@ from pathlib import Path
from typing import Optional
from django.conf import settings
from sklearn.exceptions import InconsistentVersionWarning
from documents.models import Document
from documents.models import MatchingModel
@ -18,7 +19,9 @@ logger = logging.getLogger("paperless.classifier")
class IncompatibleClassifierVersionError(Exception):
pass
def __init__(self, message: str, *args: object) -> None:
self.message = message
super().__init__(*args)
class ClassifierModelCorruptError(Exception):
@ -37,8 +40,8 @@ def load_classifier() -> Optional["DocumentClassifier"]:
try:
classifier.load()
except IncompatibleClassifierVersionError:
logger.info("Classifier version updated, will re-train")
except IncompatibleClassifierVersionError as e:
logger.info(f"Classifier version incompatible: {e.message}, will re-train")
os.unlink(settings.MODEL_FILE)
classifier = None
except ClassifierModelCorruptError:
@ -114,10 +117,12 @@ class DocumentClassifier:
"#security-maintainability-limitations"
)
for warning in w:
if issubclass(warning.category, UserWarning):
w_msg = str(warning.message)
if sk_learn_warning_url in w_msg:
raise IncompatibleClassifierVersionError
# The warning is inconsistent, the MLPClassifier is a specific warning, others have not updated yet
if issubclass(warning.category, InconsistentVersionWarning) or (
issubclass(warning.category, UserWarning)
and sk_learn_warning_url in str(warning.message)
):
raise IncompatibleClassifierVersionError("sklearn version update")
def save(self):
target_file: Path = settings.MODEL_FILE

View File

@ -114,6 +114,8 @@ class SharedByUser(Filter):
ctype = ContentType.objects.get_for_model(self.model)
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()
# see https://github.com/paperless-ngx/paperless-ngx/issues/5392, we limit subqueries
# to 1 because Postgres doesn't like returning > 1 row, but all we care about is > 0
return (
qs.filter(
owner_id=value,
@ -123,7 +125,7 @@ class SharedByUser(Filter):
UserObjectPermission.objects.filter(
content_type=ctype,
object_pk=Cast(OuterRef("pk"), CharField()),
).values("user_id"),
).values("user_id")[:1],
),
)
.annotate(
@ -131,7 +133,7 @@ class SharedByUser(Filter):
GroupObjectPermission.objects.filter(
content_type=ctype,
object_pk=Cast(OuterRef("pk"), CharField()),
).values("group_id"),
).values("group_id")[:1],
),
)
.filter(

Binary file not shown.

View File

@ -1,5 +1,6 @@
import os
import re
import shutil
from pathlib import Path
from unittest import mock
@ -649,7 +650,7 @@ class TestClassifier(DirectoriesMixin, TestCase):
Path(settings.MODEL_FILE).touch()
self.assertTrue(os.path.exists(settings.MODEL_FILE))
load.side_effect = IncompatibleClassifierVersionError()
load.side_effect = IncompatibleClassifierVersionError("Dummey Error")
self.assertIsNone(load_classifier())
self.assertFalse(os.path.exists(settings.MODEL_FILE))
@ -661,3 +662,14 @@ class TestClassifier(DirectoriesMixin, TestCase):
load.side_effect = OSError()
self.assertIsNone(load_classifier())
self.assertTrue(os.path.exists(settings.MODEL_FILE))
def test_load_old_classifier_version(self):
shutil.copy(
os.path.join(os.path.dirname(__file__), "data", "v1.17.4.model.pickle"),
self.dirs.scratch_dir,
)
with override_settings(
MODEL_FILE=self.dirs.scratch_dir / "v1.17.4.model.pickle",
):
classifier = load_classifier()
self.assertIsNone(classifier)