]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Improved statistics widget
authorshamoon <4887959+shamoon@users.noreply.github.com>
Sat, 18 Mar 2023 08:42:41 +0000 (01:42 -0700)
committershamoon <4887959+shamoon@users.noreply.github.com>
Sat, 18 Mar 2023 08:51:18 +0000 (01:51 -0700)
src-ui/messages.xlf
src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html
src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.scss
src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts
src-ui/src/theme.scss
src/documents/tests/test_api.py
src/documents/views.py

index 1f46b1d6ccaa1793ba51fcd5eefb609e8fa994a6..284beceb2f17e0c26a4db3273d19e0885ff3460f 100644 (file)
         <source>Not assigned</source>
         <context-group purpose="location">
           <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>
         <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
       </trans-unit>
           <context context-type="linenumber">1</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="1171694977288479084" datatype="html">
-        <source>Documents in inbox: <x id="INTERPOLATION" equiv-text="{{statistics?.documents_inbox}}"/></source>
+      <trans-unit id="2028517964701399614" datatype="html">
+        <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 context-type="linenumber">4</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="4207135462646354574" datatype="html">
-        <source>Total documents: <x id="INTERPOLATION" equiv-text="{{statistics?.documents_total}}"/></source>
+      <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">4</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">
index 5bba41a4dccb787d98ae95c5bc3c3a9fd3659e28..ad971b49187f81360b69d30495a24ad9f34b9206 100644 (file)
@@ -1,6 +1,46 @@
 <app-widget-frame title="Statistics" [loading]="loading" i18n-title>
   <ng-container content>
-    <p class="card-text" i18n *ngIf="statistics?.documents_inbox !== null">Documents in inbox: {{statistics?.documents_inbox}}</p>
-    <p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
+    <div class="list-group border-light">
+      <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>
 </app-widget-frame>
index 3d9f7b5e7ac59fbb0a33a2930416f73e2644317e..1b4daa30084533e8330aa89b02e2887a2a62f57d 100644 (file)
@@ -1,12 +1,25 @@
 import { HttpClient } from '@angular/common/http'
 import { Component, OnDestroy, OnInit } from '@angular/core'
 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 { DocumentListViewService } from 'src/app/services/document-list-view.service'
 import { environment } from 'src/environments/environment'
 
 export interface Statistics {
   documents_total?: 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({
@@ -19,7 +32,8 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
 
   constructor(
     private http: HttpClient,
-    private consumerStatusService: ConsumerStatusService
+    private consumerStatusService: ConsumerStatusService,
+    private documentListViewService: DocumentListViewService
   ) {}
 
   statistics: Statistics = {}
@@ -50,4 +64,17 @@ export class StatisticsWidgetComponent implements OnInit, OnDestroy {
   ngOnDestroy(): void {
     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
+  }
 }
index 69fbc68cc7db8223c37eebb83205c7cd8932fec3..39071e41f8657e359bd0e346b236dc8eae723522 100644 (file)
@@ -223,6 +223,14 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
   .dropdown-menu {
     --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 {
index f56d0344ccc924ee59f24ebdc50723b4b594e60d..c08a029c9624c32c15c2b5db521fb7983a0b2296 100644 (file)
@@ -1039,9 +1039,24 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
 
     def test_statistics(self):
 
-        doc1 = Document.objects.create(title="none1", checksum="A")
-        doc2 = Document.objects.create(title="none2", checksum="B")
-        doc3 = Document.objects.create(title="none3", checksum="C")
+        doc1 = Document.objects.create(
+            title="none1",
+            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)
 
@@ -1051,6 +1066,16 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.data["documents_total"], 3)
         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):
         Document.objects.create(title="none1", checksum="A")
@@ -1058,6 +1083,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
         response = self.client.get("/api/statistics/")
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.data["documents_inbox"], None)
+        self.assertEqual(response.data["inbox_tag"], None)
 
     @mock.patch("documents.views.consume_file.delay")
     def test_upload(self, m):
index e7b0fdaa5c1ab4c45b950cceecb9d6f8a3701a11..ede5e0d90d7228b8725b69e815f3b4c72f0e5aa6 100644 (file)
@@ -21,6 +21,7 @@ from django.db.models import Count
 from django.db.models import IntegerField
 from django.db.models import Max
 from django.db.models import When
+from django.db.models.functions import Length
 from django.db.models.functions import Lower
 from django.http import Http404
 from django.http import HttpResponse
@@ -186,6 +187,7 @@ class TagViewSet(ModelViewSet, PassUserMixin):
     )
 
     def get_serializer_class(self, *args, **kwargs):
+        print(self.request.version)
         if int(self.request.version) == 1:
             return TagSerializerVersion1
         else:
@@ -794,17 +796,41 @@ class StatisticsView(APIView):
 
     def get(self, request, format=None):
         documents_total = Document.objects.all().count()
-        if Tag.objects.filter(is_inbox_tag=True).exists():
-            documents_inbox = (
-                Document.objects.filter(tags__is_inbox_tag=True).distinct().count()
+
+        inbox_tag = Tag.objects.filter(is_inbox_tag=True)
+
+        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:
-            documents_inbox = None
+            if documents_total > 0
+            else 0
+        )
 
         return Response(
             {
                 "documents_total": documents_total,
                 "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,
             },
         )