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>
|
||||
<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) {
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
}
|
||||
<span class="badge text-bg-secondary text-uppercase">{{ statusLabel(bundle.status) }}</span>
|
||||
@if (bundle.status !== statuses.Failed) {
|
||||
<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>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@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],
|
||||
status: ShareLinkBundleStatus.Pending,
|
||||
file_version: FileVersion.Archive,
|
||||
last_error: undefined,
|
||||
...overrides,
|
||||
}) as ShareLinkBundleSummary
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { CommonModule } from '@angular/common'
|
||||
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 { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
|
||||
import { FileVersion } from 'src/app/data/share-link'
|
||||
@@ -21,9 +21,11 @@ import { ConfirmButtonComponent } from '../confirm-button/confirm-button.compone
|
||||
@Component({
|
||||
selector: 'pngx-share-link-bundle-manage-dialog',
|
||||
templateUrl: './share-link-bundle-manage-dialog.component.html',
|
||||
styleUrls: ['./share-link-bundle-manage-dialog.component.scss'],
|
||||
imports: [
|
||||
ConfirmButtonComponent,
|
||||
CommonModule,
|
||||
NgbPopoverModule,
|
||||
NgxBootstrapIconsModule,
|
||||
FileSizePipe,
|
||||
],
|
||||
|
||||
@@ -7,6 +7,13 @@ export enum ShareLinkBundleStatus {
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
export type ShareLinkBundleError = {
|
||||
bundle_id: number
|
||||
message?: string
|
||||
exception_type?: string
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
export interface ShareLinkBundleSummary {
|
||||
id: number
|
||||
slug: string
|
||||
@@ -18,7 +25,7 @@ export interface ShareLinkBundleSummary {
|
||||
status: ShareLinkBundleStatus
|
||||
built_at?: string
|
||||
size_bytes?: number
|
||||
last_error?: string
|
||||
last_error?: ShareLinkBundleError
|
||||
}
|
||||
|
||||
export interface ShareLinkBundleCreatePayload {
|
||||
|
||||
@@ -120,8 +120,10 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"last_error",
|
||||
models.TextField(
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name="last error",
|
||||
),
|
||||
),
|
||||
|
||||
@@ -817,9 +817,11 @@ class ShareLinkBundle(SoftDeleteModel):
|
||||
null=True,
|
||||
)
|
||||
|
||||
last_error = models.TextField(
|
||||
last_error = models.JSONField(
|
||||
_("last error"),
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
file_path = models.CharField(
|
||||
|
||||
@@ -647,7 +647,7 @@ def build_share_link_bundle(bundle_id: int):
|
||||
|
||||
bundle.remove_file()
|
||||
bundle.status = ShareLinkBundle.Status.PROCESSING
|
||||
bundle.last_error = ""
|
||||
bundle.last_error = None
|
||||
bundle.size_bytes = None
|
||||
bundle.built_at = None
|
||||
bundle.file_path = ""
|
||||
@@ -695,7 +695,7 @@ def build_share_link_bundle(bundle_id: int):
|
||||
bundle.size_bytes = final_path.stat().st_size
|
||||
bundle.status = ShareLinkBundle.Status.READY
|
||||
bundle.built_at = timezone.now()
|
||||
bundle.last_error = ""
|
||||
bundle.last_error = None
|
||||
bundle.save(
|
||||
update_fields=[
|
||||
"file_path",
|
||||
@@ -713,7 +713,12 @@ def build_share_link_bundle(bundle_id: int):
|
||||
exc,
|
||||
)
|
||||
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"])
|
||||
try:
|
||||
temp_zip_path.unlink()
|
||||
|
||||
@@ -81,7 +81,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
||||
status=ShareLinkBundle.Status.FAILED,
|
||||
)
|
||||
bundle.documents.set([self.document])
|
||||
bundle.last_error = "Something went wrong"
|
||||
bundle.last_error = {"message": "Something went wrong"}
|
||||
bundle.size_bytes = 100
|
||||
bundle.file_path = "path/to/file.zip"
|
||||
bundle.save()
|
||||
@@ -91,7 +91,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
bundle.refresh_from_db()
|
||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING)
|
||||
self.assertEqual(bundle.last_error, "")
|
||||
self.assertIsNone(bundle.last_error)
|
||||
self.assertIsNone(bundle.size_bytes)
|
||||
self.assertEqual(bundle.file_path, "")
|
||||
delay_mock.assert_called_once_with(bundle.pk)
|
||||
@@ -172,7 +172,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
||||
file_version=ShareLink.FileVersion.ARCHIVE,
|
||||
status=ShareLinkBundle.Status.FAILED,
|
||||
file_path=str(bundle_path.relative_to(settings.MEDIA_ROOT)),
|
||||
last_error="Boom",
|
||||
last_error={"message": "Boom"},
|
||||
size_bytes=10,
|
||||
)
|
||||
bundle.documents.set([self.document])
|
||||
@@ -183,7 +183,7 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
bundle.refresh_from_db()
|
||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING)
|
||||
self.assertEqual(bundle.last_error, "")
|
||||
self.assertIsNone(bundle.last_error)
|
||||
self.assertIsNone(bundle.size_bytes)
|
||||
self.assertEqual(bundle.file_path, "")
|
||||
delay_mock.assert_called_once_with(bundle.pk)
|
||||
@@ -335,7 +335,7 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase):
|
||||
|
||||
bundle.refresh_from_db()
|
||||
self.assertEqual(bundle.status, ShareLinkBundle.Status.READY)
|
||||
self.assertEqual(bundle.last_error, "")
|
||||
self.assertIsNone(bundle.last_error)
|
||||
self.assertIsNotNone(bundle.built_at)
|
||||
self.assertGreater(bundle.size_bytes or 0, 0)
|
||||
final_path = bundle.absolute_file_path
|
||||
@@ -404,7 +404,9 @@ class ShareLinkBundleBuildTaskTests(DirectoriesMixin, APITestCase):
|
||||
|
||||
bundle.refresh_from_db()
|
||||
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"))
|
||||
self.assertTrue(scratch_zips)
|
||||
for path in scratch_zips:
|
||||
|
||||
@@ -2865,7 +2865,7 @@ class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin):
|
||||
)
|
||||
bundle.remove_file()
|
||||
bundle.status = ShareLinkBundle.Status.PENDING
|
||||
bundle.last_error = ""
|
||||
bundle.last_error = None
|
||||
bundle.size_bytes = None
|
||||
bundle.built_at = None
|
||||
bundle.file_path = ""
|
||||
@@ -2898,7 +2898,7 @@ class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin):
|
||||
)
|
||||
bundle.remove_file()
|
||||
bundle.status = ShareLinkBundle.Status.PENDING
|
||||
bundle.last_error = ""
|
||||
bundle.last_error = None
|
||||
bundle.size_bytes = None
|
||||
bundle.built_at = None
|
||||
bundle.file_path = ""
|
||||
@@ -2958,7 +2958,7 @@ class SharedLinkView(View):
|
||||
if bundle.status == ShareLinkBundle.Status.FAILED:
|
||||
bundle.remove_file()
|
||||
bundle.status = ShareLinkBundle.Status.PENDING
|
||||
bundle.last_error = ""
|
||||
bundle.last_error = None
|
||||
bundle.size_bytes = None
|
||||
bundle.built_at = None
|
||||
bundle.file_path = ""
|
||||
@@ -2982,7 +2982,7 @@ class SharedLinkView(View):
|
||||
file_path = bundle.absolute_file_path
|
||||
if file_path is None or not file_path.exists():
|
||||
bundle.status = ShareLinkBundle.Status.PENDING
|
||||
bundle.last_error = ""
|
||||
bundle.last_error = None
|
||||
bundle.size_bytes = None
|
||||
bundle.built_at = None
|
||||
bundle.file_path = ""
|
||||
|
||||
Reference in New Issue
Block a user