mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-26 22:49:01 -06:00
Merge branch 'dev' into feature-6978-sharelink-bundle
This commit is contained in:
@@ -3444,7 +3444,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">111</context>
|
<context context-type="linenumber">113</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/date/date.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/date/date.component.html</context>
|
||||||
@@ -3704,14 +3704,14 @@
|
|||||||
<source>This month</source>
|
<source>This month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">106</context>
|
<context context-type="linenumber">107</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4498682414491138092" datatype="html">
|
<trans-unit id="4498682414491138092" datatype="html">
|
||||||
<source>Yesterday</source>
|
<source>Yesterday</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">116</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context>
|
<context context-type="sourcefile">src/app/pipes/custom-date.pipe.ts</context>
|
||||||
@@ -3722,28 +3722,28 @@
|
|||||||
<source>Previous week</source>
|
<source>Previous week</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">121</context>
|
<context context-type="linenumber">123</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8586908745456864217" datatype="html">
|
<trans-unit id="8586908745456864217" datatype="html">
|
||||||
<source>Previous month</source>
|
<source>Previous month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">135</context>
|
<context context-type="linenumber">137</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="357608474534295480" datatype="html">
|
<trans-unit id="357608474534295480" datatype="html">
|
||||||
<source>Previous quarter</source>
|
<source>Previous quarter</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">141</context>
|
<context context-type="linenumber">143</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="100513227838842152" datatype="html">
|
<trans-unit id="100513227838842152" datatype="html">
|
||||||
<source>Previous year</source>
|
<source>Previous year</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">155</context>
|
<context context-type="linenumber">157</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8743659855412792665" datatype="html">
|
<trans-unit id="8743659855412792665" datatype="html">
|
||||||
|
|||||||
@@ -164,9 +164,11 @@
|
|||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<span class="ms-auto text-muted small">
|
<span class="ms-auto text-muted small">
|
||||||
@if (item.dateEnd) {
|
@if (item.dateEnd) {
|
||||||
{{ item.date | customDate:'MMM d' }} – {{ item.dateEnd | customDate:'mediumDate' }}
|
{{ item.date | customDate:'mediumDate' }} – {{ item.dateEnd | customDate:'mediumDate' }}
|
||||||
|
} @else if (item.dateTilNow) {
|
||||||
|
{{ item.dateTilNow | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||||
} @else {
|
} @else {
|
||||||
{{ item.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
{{ item.date | customDate:'mediumDate' }}
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,32 +79,34 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
|||||||
{
|
{
|
||||||
id: RelativeDate.WITHIN_1_WEEK,
|
id: RelativeDate.WITHIN_1_WEEK,
|
||||||
name: $localize`Within 1 week`,
|
name: $localize`Within 1 week`,
|
||||||
date: new Date().setDate(new Date().getDate() - 7),
|
dateTilNow: new Date().setDate(new Date().getDate() - 7),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: RelativeDate.WITHIN_1_MONTH,
|
id: RelativeDate.WITHIN_1_MONTH,
|
||||||
name: $localize`Within 1 month`,
|
name: $localize`Within 1 month`,
|
||||||
date: new Date().setMonth(new Date().getMonth() - 1),
|
dateTilNow: new Date().setMonth(new Date().getMonth() - 1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: RelativeDate.WITHIN_3_MONTHS,
|
id: RelativeDate.WITHIN_3_MONTHS,
|
||||||
name: $localize`Within 3 months`,
|
name: $localize`Within 3 months`,
|
||||||
date: new Date().setMonth(new Date().getMonth() - 3),
|
dateTilNow: new Date().setMonth(new Date().getMonth() - 3),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: RelativeDate.WITHIN_1_YEAR,
|
id: RelativeDate.WITHIN_1_YEAR,
|
||||||
name: $localize`Within 1 year`,
|
name: $localize`Within 1 year`,
|
||||||
date: new Date().setFullYear(new Date().getFullYear() - 1),
|
dateTilNow: new Date().setFullYear(new Date().getFullYear() - 1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: RelativeDate.THIS_YEAR,
|
id: RelativeDate.THIS_YEAR,
|
||||||
name: $localize`This year`,
|
name: $localize`This year`,
|
||||||
date: new Date('1/1/' + new Date().getFullYear()),
|
date: new Date('1/1/' + new Date().getFullYear()),
|
||||||
|
dateEnd: new Date('12/31/' + new Date().getFullYear()),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: RelativeDate.THIS_MONTH,
|
id: RelativeDate.THIS_MONTH,
|
||||||
name: $localize`This month`,
|
name: $localize`This month`,
|
||||||
date: new Date().setDate(1),
|
date: new Date().setDate(1),
|
||||||
|
dateEnd: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: RelativeDate.TODAY,
|
id: RelativeDate.TODAY,
|
||||||
|
|||||||
25
src/documents/migrations/0007_document_content_length.py
Normal file
25
src/documents/migrations/0007_document_content_length.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2026-01-24 07:33
|
||||||
|
|
||||||
|
import django.db.models.functions.text
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "0006_alter_document_checksum_unique"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="document",
|
||||||
|
name="content_length",
|
||||||
|
field=models.GeneratedField(
|
||||||
|
db_persist=True,
|
||||||
|
expression=django.db.models.functions.text.Length("content"),
|
||||||
|
null=False,
|
||||||
|
help_text="Length of the content field in characters. Automatically maintained by the database for faster statistics computation.",
|
||||||
|
output_field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -20,7 +20,9 @@ if settings.AUDIT_LOG_ENABLED:
|
|||||||
from auditlog.registry import auditlog
|
from auditlog.registry import auditlog
|
||||||
|
|
||||||
from django.db.models import Case
|
from django.db.models import Case
|
||||||
|
from django.db.models import PositiveIntegerField
|
||||||
from django.db.models.functions import Cast
|
from django.db.models.functions import Cast
|
||||||
|
from django.db.models.functions import Length
|
||||||
from django.db.models.functions import Substr
|
from django.db.models.functions import Substr
|
||||||
from django_softdelete.models import SoftDeleteModel
|
from django_softdelete.models import SoftDeleteModel
|
||||||
|
|
||||||
@@ -192,6 +194,15 @@ class Document(SoftDeleteModel, ModelWithOwner):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
content_length = models.GeneratedField(
|
||||||
|
expression=Length("content"),
|
||||||
|
output_field=PositiveIntegerField(default=0),
|
||||||
|
db_persist=True,
|
||||||
|
null=False,
|
||||||
|
serialize=False,
|
||||||
|
help_text="Length of the content field in characters. Automatically maintained by the database for faster statistics computation.",
|
||||||
|
)
|
||||||
|
|
||||||
mime_type = models.CharField(_("mime type"), max_length=256, editable=False)
|
mime_type = models.CharField(_("mime type"), max_length=256, editable=False)
|
||||||
|
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField(
|
||||||
@@ -1057,7 +1068,7 @@ if settings.AUDIT_LOG_ENABLED:
|
|||||||
auditlog.register(
|
auditlog.register(
|
||||||
Document,
|
Document,
|
||||||
m2m_fields={"tags"},
|
m2m_fields={"tags"},
|
||||||
exclude_fields=["modified"],
|
exclude_fields=["content_length", "modified"],
|
||||||
)
|
)
|
||||||
auditlog.register(Correspondent)
|
auditlog.register(Correspondent)
|
||||||
auditlog.register(Tag)
|
auditlog.register(Tag)
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
self.assertIn("content", results_full[0])
|
self.assertIn("content", results_full[0])
|
||||||
self.assertIn("id", results_full[0])
|
self.assertIn("id", results_full[0])
|
||||||
|
|
||||||
|
# Content length is used internally for performance reasons.
|
||||||
|
# No need to expose this field.
|
||||||
|
self.assertNotIn("content_length", results_full[0])
|
||||||
|
|
||||||
response = self.client.get("/api/documents/?fields=id", format="json")
|
response = self.client.get("/api/documents/?fields=id", format="json")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
|
|||||||
@@ -241,6 +241,10 @@ class TestExportImport(
|
|||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
self.assertEqual(checksum, element["fields"]["checksum"])
|
self.assertEqual(checksum, element["fields"]["checksum"])
|
||||||
|
|
||||||
|
# Generated field "content_length" should not be exported,
|
||||||
|
# it is automatically computed during import.
|
||||||
|
self.assertNotIn("content_length", element["fields"])
|
||||||
|
|
||||||
if document_exporter.EXPORTER_ARCHIVE_NAME in element:
|
if document_exporter.EXPORTER_ARCHIVE_NAME in element:
|
||||||
fname = (
|
fname = (
|
||||||
self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
|
self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ from django.db.models import Model
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
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.db.models.manager import Manager
|
from django.db.models.manager import Manager
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse
|
||||||
@@ -2332,23 +2331,19 @@ class StatisticsView(GenericAPIView):
|
|||||||
user = request.user if request.user is not None else None
|
user = request.user if request.user is not None else None
|
||||||
|
|
||||||
documents = (
|
documents = (
|
||||||
(
|
Document.objects.all()
|
||||||
Document.objects.all()
|
if user is None
|
||||||
if user is None
|
else get_objects_for_user_owner_aware(
|
||||||
else get_objects_for_user_owner_aware(
|
user,
|
||||||
user,
|
"documents.view_document",
|
||||||
"documents.view_document",
|
Document,
|
||||||
Document,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.only("mime_type", "content")
|
|
||||||
.prefetch_related("tags")
|
|
||||||
)
|
)
|
||||||
tags = (
|
tags = (
|
||||||
Tag.objects.all()
|
Tag.objects.all()
|
||||||
if user is None
|
if user is None
|
||||||
else get_objects_for_user_owner_aware(user, "documents.view_tag", Tag)
|
else get_objects_for_user_owner_aware(user, "documents.view_tag", Tag)
|
||||||
)
|
).only("id", "is_inbox_tag")
|
||||||
correspondent_count = (
|
correspondent_count = (
|
||||||
Correspondent.objects.count()
|
Correspondent.objects.count()
|
||||||
if user is None
|
if user is None
|
||||||
@@ -2377,31 +2372,33 @@ class StatisticsView(GenericAPIView):
|
|||||||
).count()
|
).count()
|
||||||
)
|
)
|
||||||
|
|
||||||
documents_total = documents.count()
|
inbox_tag_pks = list(
|
||||||
|
tags.filter(is_inbox_tag=True).values_list("pk", flat=True),
|
||||||
inbox_tags = tags.filter(is_inbox_tag=True)
|
)
|
||||||
|
|
||||||
documents_inbox = (
|
documents_inbox = (
|
||||||
documents.filter(tags__id__in=inbox_tags).distinct().count()
|
documents.filter(tags__id__in=inbox_tag_pks).values("id").distinct().count()
|
||||||
if inbox_tags.exists()
|
if inbox_tag_pks
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
document_file_type_counts = (
|
# Single SQL request for document stats and mime type counts
|
||||||
|
mime_type_stats = list(
|
||||||
documents.values("mime_type")
|
documents.values("mime_type")
|
||||||
.annotate(mime_type_count=Count("mime_type"))
|
.annotate(
|
||||||
.order_by("-mime_type_count")
|
mime_type_count=Count("id"),
|
||||||
if documents_total > 0
|
mime_type_chars=Sum("content_length"),
|
||||||
else []
|
)
|
||||||
|
.order_by("-mime_type_count"),
|
||||||
)
|
)
|
||||||
|
|
||||||
character_count = (
|
# Calculate totals from grouped results
|
||||||
documents.annotate(
|
documents_total = sum(row["mime_type_count"] for row in mime_type_stats)
|
||||||
characters=Length("content"),
|
character_count = sum(row["mime_type_chars"] or 0 for row in mime_type_stats)
|
||||||
)
|
document_file_type_counts = [
|
||||||
.aggregate(Sum("characters"))
|
{"mime_type": row["mime_type"], "mime_type_count": row["mime_type_count"]}
|
||||||
.get("characters__sum")
|
for row in mime_type_stats
|
||||||
)
|
]
|
||||||
|
|
||||||
current_asn = Document.objects.aggregate(
|
current_asn = Document.objects.aggregate(
|
||||||
Max("archive_serial_number", default=0),
|
Max("archive_serial_number", default=0),
|
||||||
@@ -2414,11 +2411,9 @@ class StatisticsView(GenericAPIView):
|
|||||||
"documents_total": documents_total,
|
"documents_total": documents_total,
|
||||||
"documents_inbox": documents_inbox,
|
"documents_inbox": documents_inbox,
|
||||||
"inbox_tag": (
|
"inbox_tag": (
|
||||||
inbox_tags.first().pk if inbox_tags.exists() else None
|
inbox_tag_pks[0] if inbox_tag_pks else None
|
||||||
), # backwards compatibility
|
), # backwards compatibility
|
||||||
"inbox_tags": (
|
"inbox_tags": (inbox_tag_pks if inbox_tag_pks else None),
|
||||||
[tag.pk for tag in inbox_tags] if inbox_tags.exists() else None
|
|
||||||
),
|
|
||||||
"document_file_type_counts": document_file_type_counts,
|
"document_file_type_counts": document_file_type_counts,
|
||||||
"character_count": character_count,
|
"character_count": character_count,
|
||||||
"tag_count": len(tags),
|
"tag_count": len(tags),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user