mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-28 01:26:14 +00:00
Compare commits
12 Commits
v2.18.2
...
feature-in
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f20b224ef5 | ||
![]() |
8344a6e0e3 | ||
![]() |
10a8a5da19 | ||
![]() |
d190b50032 | ||
![]() |
fda29f51c3 | ||
![]() |
f2fabc81d4 | ||
![]() |
f94c3eeea8 | ||
![]() |
22064ed004 | ||
![]() |
23daa0b974 | ||
![]() |
7b63f5a98c | ||
![]() |
7c76377477 | ||
![]() |
56c70bf177 |
@@ -1,5 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 2.18.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: prevent loss of changes when switching between open docs [@shamoon](https://github.com/shamoon) ([#10659](https://github.com/paperless-ngx/paperless-ngx/pull/10659))
|
||||||
|
- Fix: ignore incomplete tasks for system status 'last run' [@shamoon](https://github.com/shamoon) ([#10641](https://github.com/paperless-ngx/paperless-ngx/pull/10641))
|
||||||
|
- Fix: increase legibility of date filter clear button in light mode [@shamoon](https://github.com/shamoon) ([#10649](https://github.com/paperless-ngx/paperless-ngx/pull/10649))
|
||||||
|
- Fix: ensure saved view count is visible with long names [@shamoon](https://github.com/shamoon) ([#10616](https://github.com/paperless-ngx/paperless-ngx/pull/10616))
|
||||||
|
- Tweak: improve dateparser auto-detection messages [@shamoon](https://github.com/shamoon) ([#10640](https://github.com/paperless-ngx/paperless-ngx/pull/10640))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps): Bump the development group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#10578](https://github.com/paperless-ngx/paperless-ngx/pull/10578))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>6 changes</summary>
|
||||||
|
|
||||||
|
- Fix: prevent loss of changes when switching between open docs [@shamoon](https://github.com/shamoon) ([#10659](https://github.com/paperless-ngx/paperless-ngx/pull/10659))
|
||||||
|
- Fix: ignore incomplete tasks for system status 'last run' [@shamoon](https://github.com/shamoon) ([#10641](https://github.com/paperless-ngx/paperless-ngx/pull/10641))
|
||||||
|
- Tweak: improve dateparser auto-detection messages [@shamoon](https://github.com/shamoon) ([#10640](https://github.com/paperless-ngx/paperless-ngx/pull/10640))
|
||||||
|
- Fix: increase legibility of date filter clear button in light mode [@shamoon](https://github.com/shamoon) ([#10649](https://github.com/paperless-ngx/paperless-ngx/pull/10649))
|
||||||
|
- Fix: ensure saved view count is visible with long names [@shamoon](https://github.com/shamoon) ([#10616](https://github.com/paperless-ngx/paperless-ngx/pull/10616))
|
||||||
|
- Chore(deps): Bump the development group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#10578](https://github.com/paperless-ngx/paperless-ngx/pull/10578))
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 2.18.1
|
## paperless-ngx 2.18.1
|
||||||
|
|
||||||
### Features / Enhancements
|
### Features / Enhancements
|
||||||
|
@@ -113,7 +113,7 @@
|
|||||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
popoverClass="popover-slim">
|
||||||
<i-bs class="me-1" name="funnel"></i-bs>
|
<i-bs class="me-1" name="funnel"></i-bs>
|
||||||
<span> <div class="d-inline-flex view-name"><span [class.text-truncate]="!slimSidebarEnabled">{{view.name}}</span></div>
|
<span> <div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-truncate]="!slimSidebarEnabled">{{view.name}}</span></div>
|
||||||
@if (showSidebarCounts && !slimSidebarEnabled) {
|
@if (showSidebarCounts && !slimSidebarEnabled) {
|
||||||
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||||
popoverClass="popover-slim">
|
popoverClass="popover-slim">
|
||||||
<i-bs class="me-1" name="file-text"></i-bs><span> {{d.title | documentTitle}}</span>
|
<i-bs class="me-1" name="file-text"></i-bs><span> {{d.title | documentTitle}}</span>
|
||||||
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
|
<span class="close flex-column justify-content-center" (click)="closeDocument(d); $event.preventDefault()">
|
||||||
<i-bs name="x"></i-bs>
|
<i-bs name="x"></i-bs>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -191,7 +191,7 @@ main {
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
||||||
&:hover .close {
|
&:hover .close {
|
||||||
display: block;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
<pngx-page-header [(title)]="title">
|
<pngx-page-header [(title)]="title">
|
||||||
|
@if (document?.in_process) {
|
||||||
|
<span class="badge bg-danger text-dark ms-2 d-flex align-items-center">
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
@if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
|
@if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
|
||||||
@if (previewNumPages) {
|
@if (previewNumPages) {
|
||||||
<div class="input-group input-group-sm d-none d-md-flex">
|
<div class="input-group input-group-sm d-none d-md-flex">
|
||||||
@@ -50,7 +55,7 @@
|
|||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
|
||||||
<button ngbDropdownItem (click)="reprocess()" [disabled]="!userCanEdit || !userIsOwner">
|
<button ngbDropdownItem (click)="reprocess()" [disabled]="!userCanEdit || !userIsOwner || document?.in_process">
|
||||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <span i18n>Reprocess</span>
|
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <span i18n>Reprocess</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@
|
|||||||
<i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span>
|
<i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF || document?.in_process">
|
||||||
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +95,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@@ -452,6 +452,18 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true })
|
expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should navigate to 404 if error on load', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||||
|
.mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' })))
|
||||||
|
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||||
|
jest
|
||||||
|
.spyOn(documentService, 'get')
|
||||||
|
.mockReturnValue(throwError(() => new Error('not found')))
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true })
|
||||||
|
})
|
||||||
|
|
||||||
it('should support save, close and show success toast', () => {
|
it('should support save, close and show success toast', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
component.title = 'Foo Bar'
|
component.title = 'Foo Bar'
|
||||||
@@ -1388,4 +1400,19 @@ describe('DocumentDetailComponent', () => {
|
|||||||
component.openEmailDocument()
|
component.openEmailDocument()
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should set previewText', () => {
|
||||||
|
initNormally()
|
||||||
|
const previewText = 'Hello world, this is a test'
|
||||||
|
httpTestingController.expectOne(component.previewUrl).flush(previewText)
|
||||||
|
expect(component.previewText).toEqual(previewText)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set previewText to error message if preview fails', () => {
|
||||||
|
initNormally()
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(component.previewUrl)
|
||||||
|
.flush('fail', { status: 500, statusText: 'Server Error' })
|
||||||
|
expect(component.previewText).toContain('An error occurred loading content')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -15,8 +15,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-10">
|
<div class="col col-md-10">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@if (document?.in_process) {
|
||||||
|
<span class="badge bg-secondary text-light mb-2">
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title w-100">
|
<h5 class="card-title w-100">
|
||||||
@if (document) {
|
@if (document) {
|
||||||
|
@@ -37,6 +37,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="card-body bg-light p-2">
|
<div class="card-body bg-light p-2">
|
||||||
|
@if (document?.in_process) {
|
||||||
|
<span class="badge bg-secondary text-light mb-2">
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
@if (document) {
|
@if (document) {
|
||||||
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||||
|
@@ -301,6 +301,11 @@
|
|||||||
}
|
}
|
||||||
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
|
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||||
<td width="30%">
|
<td width="30%">
|
||||||
|
@if (d.in_process) {
|
||||||
|
<span class="badge bg-secondary text-light me-1">
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
<div class="d-inline-block" (mouseleave)="popupPreview.close()">
|
<div class="d-inline-block" (mouseleave)="popupPreview.close()">
|
||||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||||
|
@@ -159,6 +159,8 @@ export interface Document extends ObjectWithPermissions {
|
|||||||
|
|
||||||
page_count?: number
|
page_count?: number
|
||||||
|
|
||||||
|
in_process?: boolean
|
||||||
|
|
||||||
// Frontend only
|
// Frontend only
|
||||||
__changedFields?: string[]
|
__changedFields?: string[]
|
||||||
}
|
}
|
||||||
|
@@ -283,6 +283,7 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]:
|
|||||||
f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.",
|
f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.",
|
||||||
)
|
)
|
||||||
qs = Document.objects.filter(id__in=doc_ids)
|
qs = Document.objects.filter(id__in=doc_ids)
|
||||||
|
Document.objects.filter(pk__in=doc_ids).update(in_process=True)
|
||||||
affected_docs: list[int] = []
|
affected_docs: list[int] = []
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
|
||||||
@@ -309,7 +310,9 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]:
|
|||||||
f"Rotated document {doc.id} by {degrees} degrees",
|
f"Rotated document {doc.id} by {degrees} degrees",
|
||||||
)
|
)
|
||||||
affected_docs.append(doc.id)
|
affected_docs.append(doc.id)
|
||||||
|
Document.objects.filter(pk__in=doc_ids).update(in_process=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
Document.objects.filter(pk__in=doc_ids).update(in_process=False)
|
||||||
logger.exception(f"Error rotating document {doc.id}: {e}")
|
logger.exception(f"Error rotating document {doc.id}: {e}")
|
||||||
|
|
||||||
if len(affected_docs) > 0:
|
if len(affected_docs) > 0:
|
||||||
@@ -474,6 +477,7 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
|
|||||||
f"Attempting to delete pages {pages} from {len(doc_ids)} documents",
|
f"Attempting to delete pages {pages} from {len(doc_ids)} documents",
|
||||||
)
|
)
|
||||||
doc = Document.objects.get(id=doc_ids[0])
|
doc = Document.objects.get(id=doc_ids[0])
|
||||||
|
Document.objects.filter(pk=doc.id).update(in_process=True)
|
||||||
pages = sorted(pages) # sort pages to avoid index issues
|
pages = sorted(pages) # sort pages to avoid index issues
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
|
||||||
@@ -492,6 +496,7 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
|
|||||||
update_document_content_maybe_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}")
|
logger.info(f"Deleted pages {pages} from document {doc.id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
Document.objects.filter(pk=doc.id).update(in_process=False)
|
||||||
logger.exception(f"Error deleting pages from document {doc.id}: {e}")
|
logger.exception(f"Error deleting pages from document {doc.id}: {e}")
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
@@ -518,6 +523,7 @@ def edit_pdf(
|
|||||||
f"Editing PDF of document {doc_ids[0]} with {len(operations)} operations",
|
f"Editing PDF of document {doc_ids[0]} with {len(operations)} operations",
|
||||||
)
|
)
|
||||||
doc = Document.objects.get(id=doc_ids[0])
|
doc = Document.objects.get(id=doc_ids[0])
|
||||||
|
Document.objects.filter(pk=doc.id).update(in_process=True)
|
||||||
import pikepdf
|
import pikepdf
|
||||||
|
|
||||||
pdf_docs: list[pikepdf.Pdf] = []
|
pdf_docs: list[pikepdf.Pdf] = []
|
||||||
@@ -587,6 +593,7 @@ def edit_pdf(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error editing document {doc.id}: {e}")
|
logger.exception(f"Error editing document {doc.id}: {e}")
|
||||||
|
Document.objects.filter(pk=doc.id).update(in_process=False)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"An error occurred while editing the document: {e}",
|
f"An error occurred while editing the document: {e}",
|
||||||
) from e
|
) from e
|
||||||
|
23
src/documents/migrations/1069_document_in_process.py
Normal file
23
src/documents/migrations/1069_document_in_process.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-26 07:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "1068_alter_document_created"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="document",
|
||||||
|
name="in_process",
|
||||||
|
field=models.BooleanField(
|
||||||
|
db_index=True,
|
||||||
|
default=False,
|
||||||
|
help_text="Whether the document is currently being processed.",
|
||||||
|
verbose_name="in process",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -289,6 +289,13 @@ class Document(SoftDeleteModel, ModelWithOwner):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
in_process = models.BooleanField(
|
||||||
|
_("in process"),
|
||||||
|
default=False,
|
||||||
|
db_index=True,
|
||||||
|
help_text=_("Whether the document is currently being processed."),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
verbose_name = _("document")
|
verbose_name = _("document")
|
||||||
|
@@ -935,6 +935,8 @@ class DocumentSerializer(
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
in_process = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
def get_page_count(self, obj) -> int | None:
|
def get_page_count(self, obj) -> int | None:
|
||||||
return obj.page_count
|
return obj.page_count
|
||||||
|
|
||||||
@@ -1103,6 +1105,7 @@ class DocumentSerializer(
|
|||||||
"remove_inbox_tags",
|
"remove_inbox_tags",
|
||||||
"page_count",
|
"page_count",
|
||||||
"mime_type",
|
"mime_type",
|
||||||
|
"in_process",
|
||||||
)
|
)
|
||||||
list_serializer_class = OwnedObjectListSerializer
|
list_serializer_class = OwnedObjectListSerializer
|
||||||
|
|
||||||
|
@@ -250,6 +250,7 @@ def update_document_content_maybe_archive_file(document_id):
|
|||||||
it exists.
|
it exists.
|
||||||
"""
|
"""
|
||||||
document = Document.objects.get(id=document_id)
|
document = Document.objects.get(id=document_id)
|
||||||
|
Document.objects.filter(pk=document_id).update(in_process=True)
|
||||||
|
|
||||||
mime_type = document.mime_type
|
mime_type = document.mime_type
|
||||||
|
|
||||||
@@ -349,6 +350,7 @@ def update_document_content_maybe_archive_file(document_id):
|
|||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
parser.cleanup()
|
parser.cleanup()
|
||||||
|
Document.objects.filter(pk=document_id).update(in_process=False)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
@@ -88,6 +88,7 @@ class StandardPagination(PageNumberPagination):
|
|||||||
response_schema["properties"]["all"] = {
|
response_schema["properties"]["all"] = {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"example": "[1, 2, 3]",
|
"example": "[1, 2, 3]",
|
||||||
|
"items": {"type": "integer"},
|
||||||
}
|
}
|
||||||
return response_schema
|
return response_schema
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user