mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-26 22:49:01 -06:00
JSON field stuff, and make nice on the frontend
This commit is contained in:
@@ -51,14 +51,39 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
@if (bundle.status === statuses.Failed && bundle.last_error) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-link p-0 text-danger"
|
||||||
|
[ngbPopover]="errorDetail"
|
||||||
|
popoverClass="popover-sm"
|
||||||
|
triggers="mouseover:mouseleave"
|
||||||
|
placement="auto"
|
||||||
|
aria-label="View error details"
|
||||||
|
i18n-aria-label
|
||||||
|
>
|
||||||
|
<span class="badge text-bg-warning text-uppercase me-2">{{ statusLabel(bundle.status) }}</span>
|
||||||
|
<i-bs name="exclamation-triangle-fill" class="text-warning"></i-bs>
|
||||||
|
</button>
|
||||||
|
<ng-template #errorDetail>
|
||||||
|
@if (bundle.last_error.timestamp) {
|
||||||
|
<div class="text-muted small mb-1">
|
||||||
|
{{ bundle.last_error.timestamp | date: 'short' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<h6>{{ bundle.last_error.exception_type || ($localize`Unknown error`) }}</h6>
|
||||||
|
@if (bundle.last_error.message) {
|
||||||
|
<pre class="text-muted small"><code>{{ bundle.last_error.message }}</code></pre>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
}
|
||||||
@if (bundle.status === statuses.Processing || bundle.status === statuses.Pending) {
|
@if (bundle.status === statuses.Processing || bundle.status === statuses.Pending) {
|
||||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
}
|
}
|
||||||
|
@if (bundle.status !== statuses.Failed) {
|
||||||
<span class="badge text-bg-secondary text-uppercase">{{ statusLabel(bundle.status) }}</span>
|
<span class="badge text-bg-secondary text-uppercase">{{ statusLabel(bundle.status) }}</span>
|
||||||
</div>
|
|
||||||
@if (bundle.last_error && bundle.status === statuses.Failed) {
|
|
||||||
<div class="small text-danger mt-1">{{ bundle.last_error }}</div>
|
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (bundle.size_bytes !== undefined && bundle.size_bytes !== null) {
|
@if (bundle.size_bytes !== undefined && bundle.size_bytes !== null) {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
:host ::ng-deep .popover {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ describe('ShareLinkBundleManageDialogComponent', () => {
|
|||||||
documents: [1],
|
documents: [1],
|
||||||
status: ShareLinkBundleStatus.Pending,
|
status: ShareLinkBundleStatus.Pending,
|
||||||
file_version: FileVersion.Archive,
|
file_version: FileVersion.Archive,
|
||||||
|
last_error: undefined,
|
||||||
...overrides,
|
...overrides,
|
||||||
}) as ShareLinkBundleSummary
|
}) as ShareLinkBundleSummary
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Clipboard } from '@angular/cdk/clipboard'
|
import { Clipboard } from '@angular/cdk/clipboard'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
|
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
|
||||||
import { FileVersion } from 'src/app/data/share-link'
|
import { FileVersion } from 'src/app/data/share-link'
|
||||||
@@ -21,9 +21,11 @@ import { ConfirmButtonComponent } from '../confirm-button/confirm-button.compone
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-share-link-bundle-manage-dialog',
|
selector: 'pngx-share-link-bundle-manage-dialog',
|
||||||
templateUrl: './share-link-bundle-manage-dialog.component.html',
|
templateUrl: './share-link-bundle-manage-dialog.component.html',
|
||||||
|
styleUrls: ['./share-link-bundle-manage-dialog.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
NgbPopoverModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ export enum ShareLinkBundleStatus {
|
|||||||
Failed = 'failed',
|
Failed = 'failed',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ShareLinkBundleError = {
|
||||||
|
bundle_id: number
|
||||||
|
message?: string
|
||||||
|
exception_type?: string
|
||||||
|
timestamp?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShareLinkBundleSummary {
|
export interface ShareLinkBundleSummary {
|
||||||
id: number
|
id: number
|
||||||
slug: string
|
slug: string
|
||||||
@@ -18,7 +25,7 @@ export interface ShareLinkBundleSummary {
|
|||||||
status: ShareLinkBundleStatus
|
status: ShareLinkBundleStatus
|
||||||
built_at?: string
|
built_at?: string
|
||||||
size_bytes?: number
|
size_bytes?: number
|
||||||
last_error?: string
|
last_error?: ShareLinkBundleError
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShareLinkBundleCreatePayload {
|
export interface ShareLinkBundleCreatePayload {
|
||||||
|
|||||||
@@ -120,8 +120,10 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"last_error",
|
"last_error",
|
||||||
models.TextField(
|
models.JSONField(
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
verbose_name="last error",
|
verbose_name="last error",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -817,9 +817,11 @@ class ShareLinkBundle(SoftDeleteModel):
|
|||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
last_error = models.TextField(
|
last_error = models.JSONField(
|
||||||
_("last error"),
|
_("last error"),
|
||||||
blank=True,
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
file_path = models.CharField(
|
file_path = models.CharField(
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ def build_share_link_bundle(bundle_id: int):
|
|||||||
|
|
||||||
bundle.remove_file()
|
bundle.remove_file()
|
||||||
bundle.status = ShareLinkBundle.Status.PROCESSING
|
bundle.status = ShareLinkBundle.Status.PROCESSING
|
||||||
bundle.last_error = ""
|
bundle.last_error = None
|
||||||
bundle.size_bytes = None
|
bundle.size_bytes = None
|
||||||
bundle.built_at = None
|
bundle.built_at = None
|
||||||
bundle.file_path = ""
|
bundle.file_path = ""
|
||||||
@@ -695,7 +695,7 @@ def build_share_link_bundle(bundle_id: int):
|
|||||||
bundle.size_bytes = final_path.stat().st_size
|
bundle.size_bytes = final_path.stat().st_size
|
||||||
bundle.status = ShareLinkBundle.Status.READY
|
bundle.status = ShareLinkBundle.Status.READY
|
||||||
bundle.built_at = timezone.now()
|
bundle.built_at = timezone.now()
|
||||||
bundle.last_error = ""
|
bundle.last_error = None
|
||||||
bundle.save(
|
bundle.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
"file_path",
|
"file_path",
|
||||||
@@ -713,7 +713,12 @@ def build_share_link_bundle(bundle_id: int):
|
|||||||
exc,
|
exc,
|
||||||
)
|
)
|
||||||
bundle.status = ShareLinkBundle.Status.FAILED
|
bundle.status = ShareLinkBundle.Status.FAILED
|
||||||
bundle.last_error = str(exc)
|
bundle.last_error = {
|
||||||
|
"bundle_id": bundle_id,
|
||||||
|
"exception_type": exc.__class__.__name__,
|
||||||
|
"message": str(exc),
|
||||||
|
"timestamp": timezone.now().isoformat(),
|
||||||
|
}
|
||||||
bundle.save(update_fields=["status", "last_error"])
|
bundle.save(update_fields=["status", "last_error"])
|
||||||
try:
|
try:
|
||||||
temp_zip_path.unlink()
|
temp_zip_path.unlink()
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
status=ShareLinkBundle.Status.FAILED,
|
status=ShareLinkBundle.Status.FAILED,
|
||||||
)
|
)
|
||||||
bundle.documents.set([self.document])
|
bundle.documents.set([self.document])
|
||||||
bundle.last_error = "Something went wrong"
|
bundle.last_error = {"message": "Something went wrong"}
|
||||||
bundle.size_bytes = 100
|
bundle.size_bytes = 100
|
||||||
bundle.file_path = "path/to/file.zip"
|
bundle.file_path = "path/to/file.zip"
|
||||||
bundle.save()
|
bundle.save()
|
||||||
@@ -91,7 +91,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
bundle.refresh_from_db()
|
bundle.refresh_from_db()
|
||||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING)
|
self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING)
|
||||||
self.assertEqual(bundle.last_error, "")
|
self.assertIsNone(bundle.last_error)
|
||||||
self.assertIsNone(bundle.size_bytes)
|
self.assertIsNone(bundle.size_bytes)
|
||||||
self.assertEqual(bundle.file_path, "")
|
self.assertEqual(bundle.file_path, "")
|
||||||
delay_mock.assert_called_once_with(bundle.pk)
|
delay_mock.assert_called_once_with(bundle.pk)
|
||||||
@@ -172,7 +172,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
file_version=ShareLink.FileVersion.ARCHIVE,
|
file_version=ShareLink.FileVersion.ARCHIVE,
|
||||||
status=ShareLinkBundle.Status.FAILED,
|
status=ShareLinkBundle.Status.FAILED,
|
||||||
file_path=str(bundle_path.relative_to(settings.MEDIA_ROOT)),
|
file_path=str(bundle_path.relative_to(settings.MEDIA_ROOT)),
|
||||||
last_error="Boom",
|
last_error={"message": "Boom"},
|
||||||
size_bytes=10,
|
size_bytes=10,
|
||||||
)
|
)
|
||||||
bundle.documents.set([self.document])
|
bundle.documents.set([self.document])
|
||||||
@@ -183,7 +183,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
|
self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||||
bundle.refresh_from_db()
|
bundle.refresh_from_db()
|
||||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING)
|
self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING)
|
||||||
self.assertEqual(bundle.last_error, "")
|
self.assertIsNone(bundle.last_error)
|
||||||
self.assertIsNone(bundle.size_bytes)
|
self.assertIsNone(bundle.size_bytes)
|
||||||
self.assertEqual(bundle.file_path, "")
|
self.assertEqual(bundle.file_path, "")
|
||||||
delay_mock.assert_called_once_with(bundle.pk)
|
delay_mock.assert_called_once_with(bundle.pk)
|
||||||
@@ -335,7 +335,7 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
bundle.refresh_from_db()
|
bundle.refresh_from_db()
|
||||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.READY)
|
self.assertEqual(bundle.status, ShareLinkBundle.Status.READY)
|
||||||
self.assertEqual(bundle.last_error, "")
|
self.assertIsNone(bundle.last_error)
|
||||||
self.assertIsNotNone(bundle.built_at)
|
self.assertIsNotNone(bundle.built_at)
|
||||||
self.assertGreater(bundle.size_bytes or 0, 0)
|
self.assertGreater(bundle.size_bytes or 0, 0)
|
||||||
final_path = bundle.absolute_file_path
|
final_path = bundle.absolute_file_path
|
||||||
@@ -404,7 +404,9 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
bundle.refresh_from_db()
|
bundle.refresh_from_db()
|
||||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.FAILED)
|
self.assertEqual(bundle.status, ShareLinkBundle.Status.FAILED)
|
||||||
self.assertEqual(bundle.last_error, "zip failure")
|
self.assertIsInstance(bundle.last_error, dict)
|
||||||
|
self.assertEqual(bundle.last_error.get("message"), "zip failure")
|
||||||
|
self.assertEqual(bundle.last_error.get("exception_type"), "RuntimeError")
|
||||||
scratch_zips = list(Path(settings.SCRATCH_DIR).glob("*.zip"))
|
scratch_zips = list(Path(settings.SCRATCH_DIR).glob("*.zip"))
|
||||||
self.assertTrue(scratch_zips)
|
self.assertTrue(scratch_zips)
|
||||||
for path in scratch_zips:
|
for path in scratch_zips:
|
||||||
|
|||||||
@@ -2865,7 +2865,7 @@ class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin):
|
|||||||
)
|
)
|
||||||
bundle.remove_file()
|
bundle.remove_file()
|
||||||
bundle.status = ShareLinkBundle.Status.PENDING
|
bundle.status = ShareLinkBundle.Status.PENDING
|
||||||
bundle.last_error = ""
|
bundle.last_error = None
|
||||||
bundle.size_bytes = None
|
bundle.size_bytes = None
|
||||||
bundle.built_at = None
|
bundle.built_at = None
|
||||||
bundle.file_path = ""
|
bundle.file_path = ""
|
||||||
@@ -2898,7 +2898,7 @@ class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin):
|
|||||||
)
|
)
|
||||||
bundle.remove_file()
|
bundle.remove_file()
|
||||||
bundle.status = ShareLinkBundle.Status.PENDING
|
bundle.status = ShareLinkBundle.Status.PENDING
|
||||||
bundle.last_error = ""
|
bundle.last_error = None
|
||||||
bundle.size_bytes = None
|
bundle.size_bytes = None
|
||||||
bundle.built_at = None
|
bundle.built_at = None
|
||||||
bundle.file_path = ""
|
bundle.file_path = ""
|
||||||
@@ -2958,7 +2958,7 @@ class SharedLinkView(View):
|
|||||||
if bundle.status == ShareLinkBundle.Status.FAILED:
|
if bundle.status == ShareLinkBundle.Status.FAILED:
|
||||||
bundle.remove_file()
|
bundle.remove_file()
|
||||||
bundle.status = ShareLinkBundle.Status.PENDING
|
bundle.status = ShareLinkBundle.Status.PENDING
|
||||||
bundle.last_error = ""
|
bundle.last_error = None
|
||||||
bundle.size_bytes = None
|
bundle.size_bytes = None
|
||||||
bundle.built_at = None
|
bundle.built_at = None
|
||||||
bundle.file_path = ""
|
bundle.file_path = ""
|
||||||
@@ -2982,7 +2982,7 @@ class SharedLinkView(View):
|
|||||||
file_path = bundle.absolute_file_path
|
file_path = bundle.absolute_file_path
|
||||||
if file_path is None or not file_path.exists():
|
if file_path is None or not file_path.exists():
|
||||||
bundle.status = ShareLinkBundle.Status.PENDING
|
bundle.status = ShareLinkBundle.Status.PENDING
|
||||||
bundle.last_error = ""
|
bundle.last_error = None
|
||||||
bundle.size_bytes = None
|
bundle.size_bytes = None
|
||||||
bundle.built_at = None
|
bundle.built_at = None
|
||||||
bundle.file_path = ""
|
bundle.file_path = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user