mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
Improved statistics widget
This commit is contained in:
parent
741152dd50
commit
f3eedec402
@ -1826,7 +1826,7 @@
|
|||||||
<source>Not assigned</source>
|
<source>Not assigned</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">321</context>
|
<context context-type="linenumber">335</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
|
<note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -2202,20 +2202,48 @@
|
|||||||
<context context-type="linenumber">1</context>
|
<context context-type="linenumber">1</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1171694977288479084" datatype="html">
|
<trans-unit id="2028517964701399614" datatype="html">
|
||||||
<source>Documents in inbox: <x id="INTERPOLATION" equiv-text="{{statistics?.documents_inbox}}"/></source>
|
<source>Go to inbox</source>
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
|
||||||
<context context-type="linenumber">3</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4207135462646354574" datatype="html">
|
|
||||||
<source>Total documents: <x id="INTERPOLATION" equiv-text="{{statistics?.documents_total}}"/></source>
|
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">4</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="3497361602348932709" datatype="html">
|
||||||
|
<source>Documents in inbox</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
|
<context context-type="linenumber">5</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8809281703097241399" datatype="html">
|
||||||
|
<source>Go to documents</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
|
<context context-type="linenumber">8</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3823413855067727192" datatype="html">
|
||||||
|
<source>Total documents</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
|
<context context-type="linenumber">9</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6503529145162789855" datatype="html">
|
||||||
|
<source>Total characters</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
|
<context context-type="linenumber">13</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6125391559813574136" datatype="html">
|
||||||
|
<source>File types</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
|
<context context-type="linenumber">17</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="8187573012244728580" datatype="html">
|
<trans-unit id="8187573012244728580" datatype="html">
|
||||||
<source>Upload new documents</source>
|
<source>Upload new documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
|
@ -1,6 +1,46 @@
|
|||||||
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<p class="card-text" i18n *ngIf="statistics?.documents_inbox !== null">Documents in inbox: {{statistics?.documents_inbox}}</p>
|
<div class="list-group border-light">
|
||||||
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
|
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to inbox" i18n-title href="javascript:void()" *ngIf="statistics?.documents_inbox !== null" (click)="goToInbox()">
|
||||||
|
<ng-container i18n>Documents in inbox</ng-container>:
|
||||||
|
<span class="badge rounded-pill" [class.bg-primary]="statistics?.documents_inbox > 0" [class.bg-muted]="statistics?.documents_inbox === 0">{{statistics?.documents_inbox}}</span>
|
||||||
|
</a>
|
||||||
|
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" title="Go to documents" i18n-title routerLink="/documents/">
|
||||||
|
<ng-container i18n>Total documents</ng-container>:
|
||||||
|
<span class="badge bg-primary rounded-pill">{{statistics?.documents_total}}</span>
|
||||||
|
</a>
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documents/">
|
||||||
|
<ng-container i18n>Total characters</ng-container>:
|
||||||
|
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.character_count | number}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div class="flex-grow-1"><ng-container i18n>File types</ng-container>:</div>
|
||||||
|
<div class="d-flex flex-column flex-grow-1">
|
||||||
|
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index" class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fst-italic text-muted">{{filetype.mime_type}}</span>
|
||||||
|
<span class="badge bg-secondary text-light rounded-pill">{{getFileTypePercent(filetype) | number: '1.0-1'}}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="list-group-item border-dark d-flex justify-content-between align-items-center">
|
||||||
|
<span class="me-3" i18n>File types:</span>
|
||||||
|
<div class="progress flex-grow-1">
|
||||||
|
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index"
|
||||||
|
class="progress-bar bg-primary text-primary-contrast"
|
||||||
|
[class.me-1]="i < statistics?.document_file_type_counts.length - 1"
|
||||||
|
role="progressbar"
|
||||||
|
[ngbPopover]="filetype.mime_type"
|
||||||
|
i18n-ngbPopover
|
||||||
|
triggers="mouseenter:mouseleave"
|
||||||
|
[attr.aria-label]="filetype.mime_type"
|
||||||
|
[style.width]="getFileTypePercent(filetype) + '%'"
|
||||||
|
[attr.aria-valuenow]="getFileTypePercent(filetype)"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
<ng-container *ngIf="getFileTypePercent(filetype) > 25">{{filetype.mime_type}}</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-widget-frame>
|
</app-widget-frame>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.flex-column {
|
||||||
|
row-gap: 0.2rem;
|
||||||
|
}
|
@ -1,12 +1,25 @@
|
|||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { Observable, Subscription } from 'rxjs'
|
import { Observable, Subscription } from 'rxjs'
|
||||||
|
import {
|
||||||
|
FILTER_HAS_TAGS_ALL,
|
||||||
|
FILTER_IS_IN_INBOX,
|
||||||
|
} from 'src/app/data/filter-rule-type'
|
||||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||||
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
export interface Statistics {
|
export interface Statistics {
|
||||||
documents_total?: number
|
documents_total?: number
|
||||||
documents_inbox?: number
|
documents_inbox?: number
|
||||||
|
inbox_tag?: number
|
||||||
|
document_file_type_counts?: DocumentFileType[]
|
||||||
|
character_count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentFileType {
|
||||||
|
mime_type: string
|
||||||
|
mime_type_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -19,7 +32,8 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private consumerStatusService: ConsumerStatusService
|
private consumerStatusService: ConsumerStatusService,
|
||||||
|
private documentListViewService: DocumentListViewService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
statistics: Statistics = {}
|
statistics: Statistics = {}
|
||||||
@ -50,4 +64,17 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subscription.unsubscribe()
|
this.subscription.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToInbox() {
|
||||||
|
this.documentListViewService.quickFilter([
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_TAGS_ALL,
|
||||||
|
value: this.statistics.inbox_tag.toString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileTypePercent(filetype: DocumentFileType): number {
|
||||||
|
return (filetype.mime_type_count / this.statistics?.documents_total) * 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,6 +223,14 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
--bs-dropdown-color: var(--bs-body-color);
|
--bs-dropdown-color: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card .list-group-item {
|
||||||
|
--bs-border-color: rgb(var(--bs-dark-rgb));
|
||||||
|
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: rgb(var(--bs-dark-rgb)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.color-scheme-dark {
|
body.color-scheme-dark {
|
||||||
|
@ -1039,9 +1039,24 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
def test_statistics(self):
|
def test_statistics(self):
|
||||||
|
|
||||||
doc1 = Document.objects.create(title="none1", checksum="A")
|
doc1 = Document.objects.create(
|
||||||
doc2 = Document.objects.create(title="none2", checksum="B")
|
title="none1",
|
||||||
doc3 = Document.objects.create(title="none3", checksum="C")
|
checksum="A",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
content="abc",
|
||||||
|
)
|
||||||
|
doc2 = Document.objects.create(
|
||||||
|
title="none2",
|
||||||
|
checksum="B",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
content="123",
|
||||||
|
)
|
||||||
|
doc3 = Document.objects.create(
|
||||||
|
title="none3",
|
||||||
|
checksum="C",
|
||||||
|
mime_type="text/plain",
|
||||||
|
content="hello",
|
||||||
|
)
|
||||||
|
|
||||||
tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
|
tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
|
||||||
|
|
||||||
@ -1051,6 +1066,16 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["documents_total"], 3)
|
self.assertEqual(response.data["documents_total"], 3)
|
||||||
self.assertEqual(response.data["documents_inbox"], 1)
|
self.assertEqual(response.data["documents_inbox"], 1)
|
||||||
|
self.assertEqual(response.data["inbox_tag"], tag_inbox.pk)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["document_file_type_counts"][0]["mime_type_count"],
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data["document_file_type_counts"][1]["mime_type_count"],
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.data["character_count"], 11)
|
||||||
|
|
||||||
def test_statistics_no_inbox_tag(self):
|
def test_statistics_no_inbox_tag(self):
|
||||||
Document.objects.create(title="none1", checksum="A")
|
Document.objects.create(title="none1", checksum="A")
|
||||||
@ -1058,6 +1083,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
|
|||||||
response = self.client.get("/api/statistics/")
|
response = self.client.get("/api/statistics/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["documents_inbox"], None)
|
self.assertEqual(response.data["documents_inbox"], None)
|
||||||
|
self.assertEqual(response.data["inbox_tag"], None)
|
||||||
|
|
||||||
@mock.patch("documents.views.consume_file.delay")
|
@mock.patch("documents.views.consume_file.delay")
|
||||||
def test_upload(self, m):
|
def test_upload(self, m):
|
||||||
|
@ -21,6 +21,7 @@ from django.db.models import Count
|
|||||||
from django.db.models import IntegerField
|
from django.db.models import IntegerField
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.db.models import When
|
from django.db.models import When
|
||||||
|
from django.db.models.functions import Length
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
@ -186,6 +187,7 @@ class TagViewSet(ModelViewSet, PassUserMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
def get_serializer_class(self, *args, **kwargs):
|
||||||
|
print(self.request.version)
|
||||||
if int(self.request.version) == 1:
|
if int(self.request.version) == 1:
|
||||||
return TagSerializerVersion1
|
return TagSerializerVersion1
|
||||||
else:
|
else:
|
||||||
@ -794,17 +796,41 @@ class StatisticsView(APIView):
|
|||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
documents_total = Document.objects.all().count()
|
documents_total = Document.objects.all().count()
|
||||||
if Tag.objects.filter(is_inbox_tag=True).exists():
|
|
||||||
documents_inbox = (
|
inbox_tag = Tag.objects.filter(is_inbox_tag=True)
|
||||||
Document.objects.filter(tags__is_inbox_tag=True).distinct().count()
|
|
||||||
|
documents_inbox = (
|
||||||
|
Document.objects.filter(tags__is_inbox_tag=True).distinct().count()
|
||||||
|
if inbox_tag.exists()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
document_file_type_counts = (
|
||||||
|
Document.objects.values("mime_type")
|
||||||
|
.annotate(mime_type_count=Count("mime_type"))
|
||||||
|
.order_by("-mime_type_count")
|
||||||
|
if documents_total > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
character_count = (
|
||||||
|
sum(
|
||||||
|
Document.objects.annotate(characters=Length("content")).values_list(
|
||||||
|
"characters",
|
||||||
|
flat=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
if documents_total > 0
|
||||||
documents_inbox = None
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"documents_total": documents_total,
|
"documents_total": documents_total,
|
||||||
"documents_inbox": documents_inbox,
|
"documents_inbox": documents_inbox,
|
||||||
|
"inbox_tag": inbox_tag.first().pk if inbox_tag.exists() else None,
|
||||||
|
"document_file_type_counts": document_file_type_counts,
|
||||||
|
"character_count": character_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user