From 1a6f32534ce98ad208f158f6be46e59f663fbdae Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 16 May 2025 07:23:04 -0700 Subject: [PATCH 1/7] Change: treat created as date not datetime (#9793) --- src/documents/filters.py | 30 +++++++-- src/documents/index.py | 3 +- .../migrations/1067_alter_document_created.py | 62 +++++++++++++++++++ src/documents/models.py | 11 ++-- src/documents/serialisers.py | 16 ++--- src/documents/signals/handlers.py | 10 +-- src/documents/templating/filepath.py | 16 +++-- src/documents/templating/workflows.py | 3 +- src/documents/tests/test_api_documents.py | 28 ++++++++- src/documents/tests/test_api_objects.py | 5 +- src/documents/tests/test_api_permissions.py | 2 +- src/documents/tests/test_api_search.py | 2 +- src/documents/tests/test_api_trash.py | 4 ++ src/documents/tests/test_bulk_edit.py | 11 +++- src/documents/tests/test_consumer.py | 27 +++----- src/documents/tests/test_document_model.py | 56 ++--------------- src/documents/tests/test_file_handling.py | 34 +++++----- src/documents/tests/test_migration_created.py | 36 +++++++++++ 18 files changed, 228 insertions(+), 128 deletions(-) create mode 100644 src/documents/migrations/1067_alter_document_created.py create mode 100644 src/documents/tests/test_migration_created.py diff --git a/src/documents/filters.py b/src/documents/filters.py index 90161a1e6..cf51aa8e9 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -48,6 +48,15 @@ CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"] ID_KWARGS = ["in", "exact"] INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"] DATE_KWARGS = [ + "year", + "month", + "day", + "gt", + "gte", + "lt", + "lte", +] +DATETIME_KWARGS = [ "year", "month", "day", @@ -731,6 +740,19 @@ class DocumentFilterSet(FilterSet): mime_type = MimeTypeFilter() + # Backwards compatibility + created__date__gt = Filter( + field_name="created", + label="Created after", + lookup_expr="gt", + ) + + created__date__lt = Filter( + field_name="created", + label="Created before", + lookup_expr="lt", + ) + class Meta: model = Document fields = { @@ -739,8 +761,8 @@ class DocumentFilterSet(FilterSet): "content": CHAR_KWARGS, "archive_serial_number": INT_KWARGS, "created": DATE_KWARGS, - "added": DATE_KWARGS, - "modified": DATE_KWARGS, + "added": DATETIME_KWARGS, + "modified": DATETIME_KWARGS, "original_filename": CHAR_KWARGS, "checksum": CHAR_KWARGS, "correspondent": ["isnull"], @@ -764,8 +786,8 @@ class ShareLinkFilterSet(FilterSet): class Meta: model = ShareLink fields = { - "created": DATE_KWARGS, - "expiration": DATE_KWARGS, + "created": DATETIME_KWARGS, + "expiration": DATETIME_KWARGS, } diff --git a/src/documents/index.py b/src/documents/index.py index 9b3a1724c..3a2b2cb58 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -5,6 +5,7 @@ import math from collections import Counter from contextlib import contextmanager from datetime import datetime +from datetime import time from datetime import timezone from shutil import rmtree from typing import TYPE_CHECKING @@ -168,7 +169,7 @@ def update_document(writer: AsyncWriter, doc: Document) -> None: type=doc.document_type.name if doc.document_type else None, type_id=doc.document_type.id if doc.document_type else None, has_type=doc.document_type is not None, - created=doc.created, + created=datetime.combine(doc.created, time.min), added=doc.added, asn=asn, modified=doc.modified, diff --git a/src/documents/migrations/1067_alter_document_created.py b/src/documents/migrations/1067_alter_document_created.py new file mode 100644 index 000000000..82736f980 --- /dev/null +++ b/src/documents/migrations/1067_alter_document_created.py @@ -0,0 +1,62 @@ +# Generated by Django 5.1.7 on 2025-04-04 01:08 + + +import datetime + +from django.db import migrations +from django.db import models +from django.db.models.functions import TruncDate + + +def migrate_date(apps, schema_editor): + Document = apps.get_model("documents", "Document") + queryset = Document.objects.annotate( + truncated_created=TruncDate("created"), + ).values("id", "truncated_created") + + # Batch to avoid loading all objects into memory at once, + # which would be problematic for large datasets. + batch_size = 500 + updates = [] + for item in queryset.iterator(chunk_size=batch_size): + updates.append( + Document(id=item["id"], created_date=item["truncated_created"]), + ) + if len(updates) >= batch_size: + Document.objects.bulk_update(updates, ["created_date"]) + updates.clear() + if updates: + Document.objects.bulk_update(updates, ["created_date"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1066_alter_workflowtrigger_schedule_offset_days"), + ] + + operations = [ + migrations.AddField( + model_name="document", + name="created_date", + field=models.DateField(null=True), + ), + migrations.RunPython(migrate_date, reverse_code=migrations.RunPython.noop), + migrations.RemoveField( + model_name="document", + name="created", + ), + migrations.RenameField( + model_name="document", + old_name="created_date", + new_name="created", + ), + migrations.AlterField( + model_name="document", + name="created", + field=models.DateField( + db_index=True, + default=datetime.datetime.today, + verbose_name="created", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 74090700c..17d1035dd 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -213,7 +213,11 @@ class Document(SoftDeleteModel, ModelWithOwner): ), ) - created = models.DateTimeField(_("created"), default=timezone.now, db_index=True) + created = models.DateField( + _("created"), + default=datetime.datetime.today, + db_index=True, + ) modified = models.DateTimeField( _("modified"), @@ -291,8 +295,7 @@ class Document(SoftDeleteModel, ModelWithOwner): verbose_name_plural = _("documents") def __str__(self) -> str: - # Convert UTC database time to local time - created = datetime.date.isoformat(timezone.localdate(self.created)) + created = self.created.isoformat() res = f"{created}" @@ -371,7 +374,7 @@ class Document(SoftDeleteModel, ModelWithOwner): @property def created_date(self): - return timezone.localdate(self.created) + return self.created class SavedView(ModelWithOwner): diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index e340e8525..062775544 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1,10 +1,9 @@ from __future__ import annotations -import datetime import logging import math import re -import zoneinfo +from datetime import datetime from decimal import Decimal from typing import TYPE_CHECKING @@ -423,7 +422,7 @@ class OwnedObjectListSerializer(serializers.ListSerializer): class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): - last_correspondence = serializers.DateTimeField(read_only=True, required=False) + last_correspondence = serializers.DateField(read_only=True, required=False) class Meta: model = Correspondent @@ -966,11 +965,7 @@ class DocumentSerializer( def update(self, instance: Document, validated_data): if "created_date" in validated_data and "created" not in validated_data: - new_datetime = datetime.datetime.combine( - validated_data.get("created_date"), - datetime.time(0, 0, 0, 0, zoneinfo.ZoneInfo(settings.TIME_ZONE)), - ) - instance.created = new_datetime + instance.created = validated_data.get("created_date") instance.save() if "created_date" in validated_data: validated_data.pop("created_date") @@ -1646,6 +1641,11 @@ class PostDocumentSerializer(serializers.Serializer): else: return None + def validate_created(self, created): + # support datetime format for created for backwards compatibility + if isinstance(created, datetime): + return created.date() + class BulkDownloadSerializer(DocumentListSerializer): content = serializers.ChoiceField( diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 673ecba52..2cc0cbcaf 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -722,7 +722,7 @@ def run_workflows( timezone.localtime(document.added), document.original_filename or "", document.filename or "", - timezone.localtime(document.created), + document.created, ) except Exception: logger.exception( @@ -1010,7 +1010,7 @@ def run_workflows( filename = document.original_filename or "" current_filename = document.filename or "" added = timezone.localtime(document.added) - created = timezone.localtime(document.created) + created = document.created else: title = overrides.title if overrides.title else str(document.original_file) doc_url = "" @@ -1032,7 +1032,7 @@ def run_workflows( filename = document.original_file if document.original_file else "" current_filename = filename added = timezone.localtime(timezone.now()) - created = timezone.localtime(overrides.created) + created = overrides.created subject = ( parse_w_workflow_placeholders( @@ -1098,7 +1098,7 @@ def run_workflows( filename = document.original_filename or "" current_filename = document.filename or "" added = timezone.localtime(document.added) - created = timezone.localtime(document.created) + created = document.created else: title = overrides.title if overrides.title else str(document.original_file) doc_url = "" @@ -1120,7 +1120,7 @@ def run_workflows( filename = document.original_file if document.original_file else "" current_filename = filename added = timezone.localtime(timezone.now()) - created = timezone.localtime(overrides.created) + created = overrides.created try: data = {} diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 45e1cad9e..633a85cc8 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -137,16 +137,14 @@ def get_creation_date_context(document: Document) -> dict[str, str]: Given a Document, localizes the creation date and builds a context dictionary with some common, shorthand formatted values from it """ - local_created = timezone.localdate(document.created) - return { - "created": local_created.isoformat(), - "created_year": local_created.strftime("%Y"), - "created_year_short": local_created.strftime("%y"), - "created_month": local_created.strftime("%m"), - "created_month_name": local_created.strftime("%B"), - "created_month_name_short": local_created.strftime("%b"), - "created_day": local_created.strftime("%d"), + "created": document.created.isoformat(), + "created_year": document.created.strftime("%Y"), + "created_year_short": document.created.strftime("%y"), + "created_month": document.created.strftime("%m"), + "created_month_name": document.created.strftime("%B"), + "created_month_name_short": document.created.strftime("%b"), + "created_day": document.created.strftime("%d"), } diff --git a/src/documents/templating/workflows.py b/src/documents/templating/workflows.py index e256e3030..e679dbaa1 100644 --- a/src/documents/templating/workflows.py +++ b/src/documents/templating/workflows.py @@ -1,3 +1,4 @@ +from datetime import date from datetime import datetime from pathlib import Path @@ -10,7 +11,7 @@ def parse_w_workflow_placeholders( local_added: datetime, original_filename: str, filename: str, - created: datetime | None = None, + created: date | None = None, doc_title: str | None = None, doc_url: str | None = None, ) -> str: diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 0af1f5040..c63ffdb57 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -171,6 +171,32 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results[0]), 0) + def test_document_update_with_created_date(self): + """ + GIVEN: + - Existing document + WHEN: + - Document is updated with created_date and not created + THEN: + - Document created field is updated + """ + doc = Document.objects.create( + title="none", + checksum="123", + mime_type="application/pdf", + created=date(2023, 1, 1), + ) + + created_date = date(2023, 2, 1) + self.client.patch( + f"/api/documents/{doc.pk}/", + {"created_date": created_date}, + format="json", + ) + + doc.refresh_from_db() + self.assertEqual(doc.created_date, created_date) + def test_document_actions(self): _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) @@ -1313,7 +1339,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): _, overrides = self.get_last_consume_delay_call_args() - self.assertEqual(overrides.created, created) + self.assertEqual(overrides.created, created.date()) def test_upload_with_asn(self): self.consume_file_mock.return_value = celery.result.AsyncResult( diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index d4d3c729e..bba9031db 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -4,7 +4,6 @@ from unittest import mock from django.contrib.auth.models import Permission from django.contrib.auth.models import User -from django.utils import timezone from rest_framework import status from rest_framework.test import APITestCase @@ -104,13 +103,13 @@ class TestApiObjects(DirectoriesMixin, APITestCase): Document.objects.create( mime_type="application/pdf", correspondent=self.c1, - created=timezone.make_aware(datetime.datetime(2022, 1, 1)), + created=datetime.date(2022, 1, 1), checksum="123", ) Document.objects.create( mime_type="application/pdf", correspondent=self.c1, - created=timezone.make_aware(datetime.datetime(2022, 1, 2)), + created=datetime.date(2022, 1, 2), checksum="456", ) diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py index 692c22417..7f0cb2e81 100644 --- a/src/documents/tests/test_api_permissions.py +++ b/src/documents/tests/test_api_permissions.py @@ -474,7 +474,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase): self.client.force_authenticate(user1) response = self.client.get( - "/api/documents/", + "/api/documents/?ordering=-id", format="json", ) diff --git a/src/documents/tests/test_api_search.py b/src/documents/tests/test_api_search.py index 118862979..7ffce06de 100644 --- a/src/documents/tests/test_api_search.py +++ b/src/documents/tests/test_api_search.py @@ -721,7 +721,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): d3.tags.add(t2) d4 = Document.objects.create( checksum="4", - created=timezone.make_aware(datetime.datetime(2020, 7, 13)), + created=datetime.date(2020, 7, 13), content="test", original_filename="doc4.pdf", ) diff --git a/src/documents/tests/test_api_trash.py b/src/documents/tests/test_api_trash.py index ab4e96773..757728690 100644 --- a/src/documents/tests/test_api_trash.py +++ b/src/documents/tests/test_api_trash.py @@ -1,3 +1,5 @@ +from datetime import date + from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.core.cache import cache @@ -116,6 +118,7 @@ class TestTrashAPI(APITestCase): checksum="checksum", mime_type="application/pdf", owner=self.user, + created=date(2023, 1, 1), ) document_u1.delete() document_not_owned = Document.objects.create( @@ -123,6 +126,7 @@ class TestTrashAPI(APITestCase): content="content2", checksum="checksum2", mime_type="application/pdf", + created=date(2023, 1, 2), ) document_not_owned.delete() user2 = User.objects.create_user(username="user2") diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index fb84c9070..245b56ad3 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -1,4 +1,5 @@ import shutil +from datetime import date from pathlib import Path from unittest import mock @@ -39,18 +40,24 @@ class TestBulkEdit(DirectoriesMixin, TestCase): self.dt2 = DocumentType.objects.create(name="dt2") self.t1 = Tag.objects.create(name="t1") self.t2 = Tag.objects.create(name="t2") - self.doc1 = Document.objects.create(checksum="A", title="A") + self.doc1 = Document.objects.create( + checksum="A", + title="A", + created=date(2023, 1, 1), + ) self.doc2 = Document.objects.create( checksum="B", title="B", correspondent=self.c1, document_type=self.dt1, + created=date(2023, 1, 2), ) self.doc3 = Document.objects.create( checksum="C", title="C", correspondent=self.c2, document_type=self.dt2, + created=date(2023, 1, 3), ) self.doc4 = Document.objects.create(checksum="D", title="D") self.doc5 = Document.objects.create(checksum="E", title="E") @@ -529,6 +536,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): filename=sample2, mime_type="application/pdf", page_count=8, + created=date(2023, 1, 2), ) self.doc2.archive_filename = sample2_archive self.doc2.save() @@ -557,6 +565,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): title="D", filename=img_doc, mime_type="image/jpeg", + created=date(2023, 1, 3), ) self.img_doc.archive_filename = img_doc_archive self.img_doc.save() diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index 96afa61d3..370ff0ef6 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -3,12 +3,10 @@ import os import shutil import stat import tempfile -import zoneinfo from pathlib import Path from unittest import mock from unittest.mock import MagicMock -from dateutil import tz from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User @@ -247,20 +245,9 @@ class TestConsumer( self._assert_first_last_send_progress() - # Convert UTC time from DB to local time - document_date_local = timezone.localtime(document.created) - - self.assertEqual( - document_date_local.tzinfo, - zoneinfo.ZoneInfo("America/Chicago"), - ) - self.assertEqual(document_date_local.tzinfo, rough_create_date_local.tzinfo) - self.assertEqual(document_date_local.year, rough_create_date_local.year) - self.assertEqual(document_date_local.month, rough_create_date_local.month) - self.assertEqual(document_date_local.day, rough_create_date_local.day) - self.assertEqual(document_date_local.hour, rough_create_date_local.hour) - self.assertEqual(document_date_local.minute, rough_create_date_local.minute) - # Skipping seconds and more precise + self.assertEqual(document.created.year, rough_create_date_local.year) + self.assertEqual(document.created.month, rough_create_date_local.month) + self.assertEqual(document.created.day, rough_create_date_local.day) @override_settings(FILENAME_FORMAT=None) def testDeleteMacFiles(self): @@ -931,7 +918,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase): self.assertEqual( document.created, - datetime.datetime(1996, 2, 20, tzinfo=tz.gettz(settings.TIME_ZONE)), + datetime.date(1996, 2, 20), ) @override_settings(FILENAME_DATE_ORDER="YMD") @@ -961,7 +948,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase): self.assertEqual( document.created, - datetime.datetime(2022, 2, 1, tzinfo=tz.gettz(settings.TIME_ZONE)), + datetime.date(2022, 2, 1), ) def test_consume_date_filename_date_use_content(self): @@ -991,7 +978,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase): self.assertEqual( document.created, - datetime.datetime(1996, 2, 20, tzinfo=tz.gettz(settings.TIME_ZONE)), + datetime.date(1996, 2, 20), ) @override_settings( @@ -1023,7 +1010,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase): self.assertEqual( document.created, - datetime.datetime(1997, 2, 20, tzinfo=tz.gettz(settings.TIME_ZONE)), + datetime.date(1997, 2, 20), ) diff --git a/src/documents/tests/test_document_model.py b/src/documents/tests/test_document_model.py index eca08f82a..45c8441f6 100644 --- a/src/documents/tests/test_document_model.py +++ b/src/documents/tests/test_document_model.py @@ -1,12 +1,11 @@ import shutil import tempfile -import zoneinfo +from datetime import date from pathlib import Path from unittest import mock from django.test import TestCase from django.test import override_settings -from django.utils import timezone from documents.models import Correspondent from documents.models import Document @@ -81,60 +80,15 @@ class TestDocument(TestCase): doc = Document( mime_type="application/pdf", title="test", - created=timezone.datetime(2020, 12, 25, tzinfo=zoneinfo.ZoneInfo("UTC")), + created=date(2020, 12, 25), ) self.assertEqual(doc.get_public_filename(), "2020-12-25 test.pdf") - @override_settings( - TIME_ZONE="Europe/Berlin", - ) - def test_file_name_with_timezone(self): - # See https://docs.djangoproject.com/en/4.0/ref/utils/#django.utils.timezone.now - # The default for created is an aware datetime in UTC - # This does that, just manually, with a fixed date - local_create_date = timezone.datetime( - 2020, - 12, - 25, - tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"), - ) - - utc_create_date = local_create_date.astimezone(zoneinfo.ZoneInfo("UTC")) - - doc = Document( - mime_type="application/pdf", - title="test", - created=utc_create_date, - ) - - # Ensure the create date would cause an off by 1 if not properly created above - self.assertEqual(utc_create_date.date().day, 24) - self.assertEqual(doc.get_public_filename(), "2020-12-25 test.pdf") - - local_create_date = timezone.datetime( - 2020, - 1, - 1, - tzinfo=zoneinfo.ZoneInfo("Europe/Berlin"), - ) - - utc_create_date = local_create_date.astimezone(zoneinfo.ZoneInfo("UTC")) - - doc = Document( - mime_type="application/pdf", - title="test", - created=utc_create_date, - ) - - # Ensure the create date would cause an off by 1 in the year if not properly created above - self.assertEqual(utc_create_date.date().year, 2019) - self.assertEqual(doc.get_public_filename(), "2020-01-01 test.pdf") - def test_file_name_jpg(self): doc = Document( mime_type="image/jpeg", title="test", - created=timezone.datetime(2020, 12, 25, tzinfo=zoneinfo.ZoneInfo("UTC")), + created=date(2020, 12, 25), ) self.assertEqual(doc.get_public_filename(), "2020-12-25 test.jpg") @@ -142,7 +96,7 @@ class TestDocument(TestCase): doc = Document( mime_type="application/zip", title="test", - created=timezone.datetime(2020, 12, 25, tzinfo=zoneinfo.ZoneInfo("UTC")), + created=date(2020, 12, 25), ) self.assertEqual(doc.get_public_filename(), "2020-12-25 test.zip") @@ -150,6 +104,6 @@ class TestDocument(TestCase): doc = Document( mime_type="image/jpegasd", title="test", - created=timezone.datetime(2020, 12, 25, tzinfo=zoneinfo.ZoneInfo("UTC")), + created=date(2020, 12, 25), ) self.assertEqual(doc.get_public_filename(), "2020-12-25 test") diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 6d2d396fc..fe033a267 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -333,7 +333,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): self.assertEqual(generate_filename(doc1), "2020-03-06.pdf") - doc1.created = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1)) + doc1.created = datetime.date(2020, 11, 16) self.assertEqual(generate_filename(doc1), "2020-11-16.pdf") @@ -912,7 +912,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): def test_date(self): doc = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 5, 21, 7, 36, 51, 153)), + created=datetime.date(2020, 5, 21), mime_type="application/pdf", pk=2, checksum="2", @@ -930,7 +930,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), mime_type="application/pdf", pk=2, checksum="2", @@ -951,7 +951,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), mime_type="application/pdf", pk=2, checksum="2", @@ -979,7 +979,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): ) doc = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), mime_type="application/pdf", pk=2, checksum="2", @@ -1007,7 +1007,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc_a = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), mime_type="application/pdf", pk=2, checksum="2", @@ -1019,7 +1019,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): ) doc_b = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 7, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 7, 25), mime_type="application/pdf", pk=5, checksum="abcde", @@ -1047,7 +1047,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc_a = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), mime_type="application/pdf", pk=2, checksum="2", @@ -1055,7 +1055,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): ) doc_b = Document.objects.create( title="does not matter", - created=timezone.make_aware(datetime.datetime(2020, 7, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 7, 25), mime_type="application/pdf", pk=5, checksum="abcde", @@ -1074,9 +1074,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): def test_short_names_created(self): doc = Document.objects.create( title="The Title", - created=timezone.make_aware( - datetime.datetime(1989, 12, 21, 7, 36, 51, 153), - ), + created=datetime.date(1989, 12, 2), mime_type="application/pdf", pk=2, checksum="2", @@ -1236,7 +1234,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): doc_a = Document.objects.create( title="Does Matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), mime_type="application/pdf", pk=2, @@ -1302,7 +1300,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc_a = Document.objects.create( title="Does Matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), mime_type="application/pdf", pk=2, @@ -1337,7 +1335,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc_a = Document.objects.create( title="Does Matter", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), mime_type="application/pdf", pk=2, @@ -1369,7 +1367,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc_a = Document.objects.create( title="Some Title", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), mime_type="application/pdf", pk=2, @@ -1474,7 +1472,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc_a = Document.objects.create( title="Some Title", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), mime_type="application/pdf", pk=2, @@ -1529,7 +1527,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): """ doc = Document.objects.create( title="Some Title! With @ Special # Characters", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + created=datetime.date(2020, 6, 25), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), mime_type="application/pdf", pk=2, diff --git a/src/documents/tests/test_migration_created.py b/src/documents/tests/test_migration_created.py new file mode 100644 index 000000000..7aced9789 --- /dev/null +++ b/src/documents/tests/test_migration_created.py @@ -0,0 +1,36 @@ +from datetime import datetime +from datetime import timedelta + +from documents.tests.utils import DirectoriesMixin +from documents.tests.utils import TestMigrations + + +class TestMigrateDocumentCreated(DirectoriesMixin, TestMigrations): + migrate_from = "1066_alter_workflowtrigger_schedule_offset_days" + migrate_to = "1067_alter_document_created" + + def setUpBeforeMigration(self, apps): + # create 600 documents + for i in range(600): + Document = apps.get_model("documents", "Document") + Document.objects.create( + title=f"test{i}", + mime_type="application/pdf", + filename=f"file{i}.pdf", + created=datetime( + 2023, + 10, + 1, + 12, + 0, + 0, + ) + + timedelta(days=i), + checksum=i, + ) + + def testDocumentCreatedMigrated(self): + Document = self.apps.get_model("documents", "Document") + + doc = Document.objects.get(id=1) + self.assertEqual(doc.created, datetime(2023, 10, 1, 12, 0, 0).date()) From 3365fc92fdd6054e74167e88d192a51b855a7466 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 14:25:26 +0000 Subject: [PATCH 2/7] Auto translate strings --- src/locale/en_US/LC_MESSAGES/django.po | 534 ++++++++++++------------- 1 file changed, 267 insertions(+), 267 deletions(-) diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index acc6a1457..9bc32b0c3 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-11 19:44+0000\n" +"POT-Creation-Date: 2025-05-16 14:24+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -21,67 +21,67 @@ msgstr "" msgid "Documents" msgstr "" -#: documents/filters.py:374 +#: documents/filters.py:383 msgid "Value must be valid JSON." msgstr "" -#: documents/filters.py:393 +#: documents/filters.py:402 msgid "Invalid custom field query expression" msgstr "" -#: documents/filters.py:403 +#: documents/filters.py:412 msgid "Invalid expression list. Must be nonempty." msgstr "" -#: documents/filters.py:424 +#: documents/filters.py:433 msgid "Invalid logical operator {op!r}" msgstr "" -#: documents/filters.py:438 +#: documents/filters.py:447 msgid "Maximum number of query conditions exceeded." msgstr "" -#: documents/filters.py:503 +#: documents/filters.py:512 msgid "{name!r} is not a valid custom field." msgstr "" -#: documents/filters.py:540 +#: documents/filters.py:549 msgid "{data_type} does not support query expr {expr!r}." msgstr "" -#: documents/filters.py:648 +#: documents/filters.py:657 msgid "Maximum nesting depth exceeded." msgstr "" -#: documents/filters.py:827 +#: documents/filters.py:849 msgid "Custom field not found" msgstr "" -#: documents/models.py:36 documents/models.py:710 +#: documents/models.py:36 documents/models.py:713 msgid "owner" msgstr "" -#: documents/models.py:53 documents/models.py:921 +#: documents/models.py:53 documents/models.py:924 msgid "None" msgstr "" -#: documents/models.py:54 documents/models.py:922 +#: documents/models.py:54 documents/models.py:925 msgid "Any word" msgstr "" -#: documents/models.py:55 documents/models.py:923 +#: documents/models.py:55 documents/models.py:926 msgid "All words" msgstr "" -#: documents/models.py:56 documents/models.py:924 +#: documents/models.py:56 documents/models.py:927 msgid "Exact match" msgstr "" -#: documents/models.py:57 documents/models.py:925 +#: documents/models.py:57 documents/models.py:928 msgid "Regular expression" msgstr "" -#: documents/models.py:58 documents/models.py:926 +#: documents/models.py:58 documents/models.py:929 msgid "Fuzzy word" msgstr "" @@ -89,20 +89,20 @@ msgstr "" msgid "Automatic" msgstr "" -#: documents/models.py:62 documents/models.py:398 documents/models.py:1416 +#: documents/models.py:62 documents/models.py:401 documents/models.py:1419 #: paperless_mail/models.py:23 paperless_mail/models.py:143 msgid "name" msgstr "" -#: documents/models.py:64 documents/models.py:990 +#: documents/models.py:64 documents/models.py:993 msgid "match" msgstr "" -#: documents/models.py:67 documents/models.py:993 +#: documents/models.py:67 documents/models.py:996 msgid "matching algorithm" msgstr "" -#: documents/models.py:72 documents/models.py:998 +#: documents/models.py:72 documents/models.py:1001 msgid "is insensitive" msgstr "" @@ -168,7 +168,7 @@ msgstr "" msgid "title" msgstr "" -#: documents/models.py:170 documents/models.py:624 +#: documents/models.py:170 documents/models.py:627 msgid "content" msgstr "" @@ -206,986 +206,986 @@ msgstr "" msgid "The number of pages of the document." msgstr "" -#: documents/models.py:216 documents/models.py:630 documents/models.py:668 -#: documents/models.py:739 documents/models.py:797 +#: documents/models.py:217 documents/models.py:633 documents/models.py:671 +#: documents/models.py:742 documents/models.py:800 msgid "created" msgstr "" -#: documents/models.py:219 +#: documents/models.py:223 msgid "modified" msgstr "" -#: documents/models.py:226 +#: documents/models.py:230 msgid "storage type" msgstr "" -#: documents/models.py:234 +#: documents/models.py:238 msgid "added" msgstr "" -#: documents/models.py:241 +#: documents/models.py:245 msgid "filename" msgstr "" -#: documents/models.py:247 +#: documents/models.py:251 msgid "Current filename in storage" msgstr "" -#: documents/models.py:251 +#: documents/models.py:255 msgid "archive filename" msgstr "" -#: documents/models.py:257 +#: documents/models.py:261 msgid "Current archive filename in storage" msgstr "" -#: documents/models.py:261 +#: documents/models.py:265 msgid "original filename" msgstr "" -#: documents/models.py:267 +#: documents/models.py:271 msgid "The original name of the file when it was uploaded" msgstr "" -#: documents/models.py:274 +#: documents/models.py:278 msgid "archive serial number" msgstr "" -#: documents/models.py:284 +#: documents/models.py:288 msgid "The position of this document in your physical document archive." msgstr "" -#: documents/models.py:290 documents/models.py:641 documents/models.py:695 -#: documents/models.py:1459 +#: documents/models.py:294 documents/models.py:644 documents/models.py:698 +#: documents/models.py:1462 msgid "document" msgstr "" -#: documents/models.py:291 +#: documents/models.py:295 msgid "documents" msgstr "" -#: documents/models.py:379 +#: documents/models.py:382 msgid "Table" msgstr "" -#: documents/models.py:380 +#: documents/models.py:383 msgid "Small Cards" msgstr "" -#: documents/models.py:381 +#: documents/models.py:384 msgid "Large Cards" msgstr "" -#: documents/models.py:384 +#: documents/models.py:387 msgid "Title" msgstr "" -#: documents/models.py:385 documents/models.py:942 +#: documents/models.py:388 documents/models.py:945 msgid "Created" msgstr "" -#: documents/models.py:386 documents/models.py:941 +#: documents/models.py:389 documents/models.py:944 msgid "Added" msgstr "" -#: documents/models.py:387 +#: documents/models.py:390 msgid "Tags" msgstr "" -#: documents/models.py:388 +#: documents/models.py:391 msgid "Correspondent" msgstr "" -#: documents/models.py:389 +#: documents/models.py:392 msgid "Document Type" msgstr "" -#: documents/models.py:390 +#: documents/models.py:393 msgid "Storage Path" msgstr "" -#: documents/models.py:391 +#: documents/models.py:394 msgid "Note" msgstr "" -#: documents/models.py:392 +#: documents/models.py:395 msgid "Owner" msgstr "" -#: documents/models.py:393 +#: documents/models.py:396 msgid "Shared" msgstr "" -#: documents/models.py:394 +#: documents/models.py:397 msgid "ASN" msgstr "" -#: documents/models.py:395 +#: documents/models.py:398 msgid "Pages" msgstr "" -#: documents/models.py:401 +#: documents/models.py:404 msgid "show on dashboard" msgstr "" -#: documents/models.py:404 +#: documents/models.py:407 msgid "show in sidebar" msgstr "" -#: documents/models.py:408 +#: documents/models.py:411 msgid "sort field" msgstr "" -#: documents/models.py:413 +#: documents/models.py:416 msgid "sort reverse" msgstr "" -#: documents/models.py:416 +#: documents/models.py:419 msgid "View page size" msgstr "" -#: documents/models.py:424 +#: documents/models.py:427 msgid "View display mode" msgstr "" -#: documents/models.py:431 +#: documents/models.py:434 msgid "Document display fields" msgstr "" -#: documents/models.py:438 documents/models.py:501 +#: documents/models.py:441 documents/models.py:504 msgid "saved view" msgstr "" -#: documents/models.py:439 +#: documents/models.py:442 msgid "saved views" msgstr "" -#: documents/models.py:447 +#: documents/models.py:450 msgid "title contains" msgstr "" -#: documents/models.py:448 +#: documents/models.py:451 msgid "content contains" msgstr "" -#: documents/models.py:449 +#: documents/models.py:452 msgid "ASN is" msgstr "" -#: documents/models.py:450 +#: documents/models.py:453 msgid "correspondent is" msgstr "" -#: documents/models.py:451 +#: documents/models.py:454 msgid "document type is" msgstr "" -#: documents/models.py:452 +#: documents/models.py:455 msgid "is in inbox" msgstr "" -#: documents/models.py:453 +#: documents/models.py:456 msgid "has tag" msgstr "" -#: documents/models.py:454 +#: documents/models.py:457 msgid "has any tag" msgstr "" -#: documents/models.py:455 +#: documents/models.py:458 msgid "created before" msgstr "" -#: documents/models.py:456 +#: documents/models.py:459 msgid "created after" msgstr "" -#: documents/models.py:457 +#: documents/models.py:460 msgid "created year is" msgstr "" -#: documents/models.py:458 +#: documents/models.py:461 msgid "created month is" msgstr "" -#: documents/models.py:459 +#: documents/models.py:462 msgid "created day is" msgstr "" -#: documents/models.py:460 +#: documents/models.py:463 msgid "added before" msgstr "" -#: documents/models.py:461 +#: documents/models.py:464 msgid "added after" msgstr "" -#: documents/models.py:462 +#: documents/models.py:465 msgid "modified before" msgstr "" -#: documents/models.py:463 +#: documents/models.py:466 msgid "modified after" msgstr "" -#: documents/models.py:464 +#: documents/models.py:467 msgid "does not have tag" msgstr "" -#: documents/models.py:465 +#: documents/models.py:468 msgid "does not have ASN" msgstr "" -#: documents/models.py:466 +#: documents/models.py:469 msgid "title or content contains" msgstr "" -#: documents/models.py:467 +#: documents/models.py:470 msgid "fulltext query" msgstr "" -#: documents/models.py:468 +#: documents/models.py:471 msgid "more like this" msgstr "" -#: documents/models.py:469 +#: documents/models.py:472 msgid "has tags in" msgstr "" -#: documents/models.py:470 +#: documents/models.py:473 msgid "ASN greater than" msgstr "" -#: documents/models.py:471 +#: documents/models.py:474 msgid "ASN less than" msgstr "" -#: documents/models.py:472 +#: documents/models.py:475 msgid "storage path is" msgstr "" -#: documents/models.py:473 +#: documents/models.py:476 msgid "has correspondent in" msgstr "" -#: documents/models.py:474 +#: documents/models.py:477 msgid "does not have correspondent in" msgstr "" -#: documents/models.py:475 +#: documents/models.py:478 msgid "has document type in" msgstr "" -#: documents/models.py:476 +#: documents/models.py:479 msgid "does not have document type in" msgstr "" -#: documents/models.py:477 +#: documents/models.py:480 msgid "has storage path in" msgstr "" -#: documents/models.py:478 +#: documents/models.py:481 msgid "does not have storage path in" msgstr "" -#: documents/models.py:479 +#: documents/models.py:482 msgid "owner is" msgstr "" -#: documents/models.py:480 +#: documents/models.py:483 msgid "has owner in" msgstr "" -#: documents/models.py:481 +#: documents/models.py:484 msgid "does not have owner" msgstr "" -#: documents/models.py:482 +#: documents/models.py:485 msgid "does not have owner in" msgstr "" -#: documents/models.py:483 +#: documents/models.py:486 msgid "has custom field value" msgstr "" -#: documents/models.py:484 +#: documents/models.py:487 msgid "is shared by me" msgstr "" -#: documents/models.py:485 +#: documents/models.py:488 msgid "has custom fields" msgstr "" -#: documents/models.py:486 +#: documents/models.py:489 msgid "has custom field in" msgstr "" -#: documents/models.py:487 +#: documents/models.py:490 msgid "does not have custom field in" msgstr "" -#: documents/models.py:488 +#: documents/models.py:491 msgid "does not have custom field" msgstr "" -#: documents/models.py:489 +#: documents/models.py:492 msgid "custom fields query" msgstr "" -#: documents/models.py:490 +#: documents/models.py:493 msgid "created to" msgstr "" -#: documents/models.py:491 +#: documents/models.py:494 msgid "created from" msgstr "" -#: documents/models.py:492 +#: documents/models.py:495 msgid "added to" msgstr "" -#: documents/models.py:493 +#: documents/models.py:496 msgid "added from" msgstr "" -#: documents/models.py:494 +#: documents/models.py:497 msgid "mime type is" msgstr "" -#: documents/models.py:504 +#: documents/models.py:507 msgid "rule type" msgstr "" -#: documents/models.py:506 +#: documents/models.py:509 msgid "value" msgstr "" -#: documents/models.py:509 +#: documents/models.py:512 msgid "filter rule" msgstr "" -#: documents/models.py:510 +#: documents/models.py:513 msgid "filter rules" msgstr "" -#: documents/models.py:534 +#: documents/models.py:537 msgid "Auto Task" msgstr "" -#: documents/models.py:535 +#: documents/models.py:538 msgid "Scheduled Task" msgstr "" -#: documents/models.py:536 +#: documents/models.py:539 msgid "Manual Task" msgstr "" -#: documents/models.py:539 +#: documents/models.py:542 msgid "Consume File" msgstr "" -#: documents/models.py:540 +#: documents/models.py:543 msgid "Train Classifier" msgstr "" -#: documents/models.py:541 +#: documents/models.py:544 msgid "Check Sanity" msgstr "" -#: documents/models.py:542 +#: documents/models.py:545 msgid "Index Optimize" msgstr "" -#: documents/models.py:547 +#: documents/models.py:550 msgid "Task ID" msgstr "" -#: documents/models.py:548 +#: documents/models.py:551 msgid "Celery ID for the Task that was run" msgstr "" -#: documents/models.py:553 +#: documents/models.py:556 msgid "Acknowledged" msgstr "" -#: documents/models.py:554 +#: documents/models.py:557 msgid "If the task is acknowledged via the frontend or API" msgstr "" -#: documents/models.py:560 +#: documents/models.py:563 msgid "Task Filename" msgstr "" -#: documents/models.py:561 +#: documents/models.py:564 msgid "Name of the file which the Task was run for" msgstr "" -#: documents/models.py:568 +#: documents/models.py:571 msgid "Task Name" msgstr "" -#: documents/models.py:569 +#: documents/models.py:572 msgid "Name of the task that was run" msgstr "" -#: documents/models.py:576 +#: documents/models.py:579 msgid "Task State" msgstr "" -#: documents/models.py:577 +#: documents/models.py:580 msgid "Current state of the task being run" msgstr "" -#: documents/models.py:583 +#: documents/models.py:586 msgid "Created DateTime" msgstr "" -#: documents/models.py:584 +#: documents/models.py:587 msgid "Datetime field when the task result was created in UTC" msgstr "" -#: documents/models.py:590 +#: documents/models.py:593 msgid "Started DateTime" msgstr "" -#: documents/models.py:591 +#: documents/models.py:594 msgid "Datetime field when the task was started in UTC" msgstr "" -#: documents/models.py:597 +#: documents/models.py:600 msgid "Completed DateTime" msgstr "" -#: documents/models.py:598 +#: documents/models.py:601 msgid "Datetime field when the task was completed in UTC" msgstr "" -#: documents/models.py:604 +#: documents/models.py:607 msgid "Result Data" msgstr "" -#: documents/models.py:606 +#: documents/models.py:609 msgid "The data returned by the task" msgstr "" -#: documents/models.py:614 +#: documents/models.py:617 msgid "Task Type" msgstr "" -#: documents/models.py:615 +#: documents/models.py:618 msgid "The type of task that was run" msgstr "" -#: documents/models.py:626 +#: documents/models.py:629 msgid "Note for the document" msgstr "" -#: documents/models.py:650 +#: documents/models.py:653 msgid "user" msgstr "" -#: documents/models.py:655 +#: documents/models.py:658 msgid "note" msgstr "" -#: documents/models.py:656 +#: documents/models.py:659 msgid "notes" msgstr "" -#: documents/models.py:664 +#: documents/models.py:667 msgid "Archive" msgstr "" -#: documents/models.py:665 +#: documents/models.py:668 msgid "Original" msgstr "" -#: documents/models.py:676 paperless_mail/models.py:75 +#: documents/models.py:679 paperless_mail/models.py:75 msgid "expiration" msgstr "" -#: documents/models.py:683 +#: documents/models.py:686 msgid "slug" msgstr "" -#: documents/models.py:715 +#: documents/models.py:718 msgid "share link" msgstr "" -#: documents/models.py:716 +#: documents/models.py:719 msgid "share links" msgstr "" -#: documents/models.py:728 +#: documents/models.py:731 msgid "String" msgstr "" -#: documents/models.py:729 +#: documents/models.py:732 msgid "URL" msgstr "" -#: documents/models.py:730 +#: documents/models.py:733 msgid "Date" msgstr "" -#: documents/models.py:731 +#: documents/models.py:734 msgid "Boolean" msgstr "" -#: documents/models.py:732 +#: documents/models.py:735 msgid "Integer" msgstr "" -#: documents/models.py:733 +#: documents/models.py:736 msgid "Float" msgstr "" -#: documents/models.py:734 +#: documents/models.py:737 msgid "Monetary" msgstr "" -#: documents/models.py:735 +#: documents/models.py:738 msgid "Document Link" msgstr "" -#: documents/models.py:736 +#: documents/models.py:739 msgid "Select" msgstr "" -#: documents/models.py:748 +#: documents/models.py:751 msgid "data type" msgstr "" -#: documents/models.py:755 +#: documents/models.py:758 msgid "extra data" msgstr "" -#: documents/models.py:759 +#: documents/models.py:762 msgid "Extra data for the custom field, such as select options" msgstr "" -#: documents/models.py:765 +#: documents/models.py:768 msgid "custom field" msgstr "" -#: documents/models.py:766 +#: documents/models.py:769 msgid "custom fields" msgstr "" -#: documents/models.py:863 +#: documents/models.py:866 msgid "custom field instance" msgstr "" -#: documents/models.py:864 +#: documents/models.py:867 msgid "custom field instances" msgstr "" -#: documents/models.py:929 +#: documents/models.py:932 msgid "Consumption Started" msgstr "" -#: documents/models.py:930 +#: documents/models.py:933 msgid "Document Added" msgstr "" -#: documents/models.py:931 +#: documents/models.py:934 msgid "Document Updated" msgstr "" -#: documents/models.py:932 +#: documents/models.py:935 msgid "Scheduled" msgstr "" -#: documents/models.py:935 +#: documents/models.py:938 msgid "Consume Folder" msgstr "" -#: documents/models.py:936 +#: documents/models.py:939 msgid "Api Upload" msgstr "" -#: documents/models.py:937 +#: documents/models.py:940 msgid "Mail Fetch" msgstr "" -#: documents/models.py:938 +#: documents/models.py:941 msgid "Web UI" msgstr "" -#: documents/models.py:943 +#: documents/models.py:946 msgid "Modified" msgstr "" -#: documents/models.py:944 +#: documents/models.py:947 msgid "Custom Field" msgstr "" -#: documents/models.py:947 +#: documents/models.py:950 msgid "Workflow Trigger Type" msgstr "" -#: documents/models.py:959 +#: documents/models.py:962 msgid "filter path" msgstr "" -#: documents/models.py:964 +#: documents/models.py:967 msgid "" "Only consume documents with a path that matches this if specified. Wildcards " "specified as * are allowed. Case insensitive." msgstr "" -#: documents/models.py:971 +#: documents/models.py:974 msgid "filter filename" msgstr "" -#: documents/models.py:976 paperless_mail/models.py:200 +#: documents/models.py:979 paperless_mail/models.py:200 msgid "" "Only consume documents which entirely match this filename if specified. " "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." msgstr "" -#: documents/models.py:987 +#: documents/models.py:990 msgid "filter documents from this mail rule" msgstr "" -#: documents/models.py:1003 +#: documents/models.py:1006 msgid "has these tag(s)" msgstr "" -#: documents/models.py:1011 +#: documents/models.py:1014 msgid "has this document type" msgstr "" -#: documents/models.py:1019 +#: documents/models.py:1022 msgid "has this correspondent" msgstr "" -#: documents/models.py:1023 +#: documents/models.py:1026 msgid "schedule offset days" msgstr "" -#: documents/models.py:1026 +#: documents/models.py:1029 msgid "The number of days to offset the schedule trigger by." msgstr "" -#: documents/models.py:1031 +#: documents/models.py:1034 msgid "schedule is recurring" msgstr "" -#: documents/models.py:1034 +#: documents/models.py:1037 msgid "If the schedule should be recurring." msgstr "" -#: documents/models.py:1039 +#: documents/models.py:1042 msgid "schedule recurring delay in days" msgstr "" -#: documents/models.py:1043 +#: documents/models.py:1046 msgid "The number of days between recurring schedule triggers." msgstr "" -#: documents/models.py:1048 +#: documents/models.py:1051 msgid "schedule date field" msgstr "" -#: documents/models.py:1053 +#: documents/models.py:1056 msgid "The field to check for a schedule trigger." msgstr "" -#: documents/models.py:1062 +#: documents/models.py:1065 msgid "schedule date custom field" msgstr "" -#: documents/models.py:1066 +#: documents/models.py:1069 msgid "workflow trigger" msgstr "" -#: documents/models.py:1067 +#: documents/models.py:1070 msgid "workflow triggers" msgstr "" -#: documents/models.py:1075 +#: documents/models.py:1078 msgid "email subject" msgstr "" -#: documents/models.py:1079 +#: documents/models.py:1082 msgid "" "The subject of the email, can include some placeholders, see documentation." msgstr "" -#: documents/models.py:1085 +#: documents/models.py:1088 msgid "email body" msgstr "" -#: documents/models.py:1088 +#: documents/models.py:1091 msgid "" "The body (message) of the email, can include some placeholders, see " "documentation." msgstr "" -#: documents/models.py:1094 +#: documents/models.py:1097 msgid "emails to" msgstr "" -#: documents/models.py:1097 +#: documents/models.py:1100 msgid "The destination email addresses, comma separated." msgstr "" -#: documents/models.py:1103 +#: documents/models.py:1106 msgid "include document in email" msgstr "" -#: documents/models.py:1114 +#: documents/models.py:1117 msgid "webhook url" msgstr "" -#: documents/models.py:1117 +#: documents/models.py:1120 msgid "The destination URL for the notification." msgstr "" -#: documents/models.py:1122 +#: documents/models.py:1125 msgid "use parameters" msgstr "" -#: documents/models.py:1127 +#: documents/models.py:1130 msgid "send as JSON" msgstr "" -#: documents/models.py:1131 +#: documents/models.py:1134 msgid "webhook parameters" msgstr "" -#: documents/models.py:1134 +#: documents/models.py:1137 msgid "The parameters to send with the webhook URL if body not used." msgstr "" -#: documents/models.py:1138 +#: documents/models.py:1141 msgid "webhook body" msgstr "" -#: documents/models.py:1141 +#: documents/models.py:1144 msgid "The body to send with the webhook URL if parameters not used." msgstr "" -#: documents/models.py:1145 +#: documents/models.py:1148 msgid "webhook headers" msgstr "" -#: documents/models.py:1148 +#: documents/models.py:1151 msgid "The headers to send with the webhook URL." msgstr "" -#: documents/models.py:1153 +#: documents/models.py:1156 msgid "include document in webhook" msgstr "" -#: documents/models.py:1164 +#: documents/models.py:1167 msgid "Assignment" msgstr "" -#: documents/models.py:1168 +#: documents/models.py:1171 msgid "Removal" msgstr "" -#: documents/models.py:1172 documents/templates/account/password_reset.html:15 +#: documents/models.py:1175 documents/templates/account/password_reset.html:15 msgid "Email" msgstr "" -#: documents/models.py:1176 +#: documents/models.py:1179 msgid "Webhook" msgstr "" -#: documents/models.py:1180 +#: documents/models.py:1183 msgid "Workflow Action Type" msgstr "" -#: documents/models.py:1186 +#: documents/models.py:1189 msgid "assign title" msgstr "" -#: documents/models.py:1191 +#: documents/models.py:1194 msgid "" "Assign a document title, can include some placeholders, see documentation." msgstr "" -#: documents/models.py:1200 paperless_mail/models.py:274 +#: documents/models.py:1203 paperless_mail/models.py:274 msgid "assign this tag" msgstr "" -#: documents/models.py:1209 paperless_mail/models.py:282 +#: documents/models.py:1212 paperless_mail/models.py:282 msgid "assign this document type" msgstr "" -#: documents/models.py:1218 paperless_mail/models.py:296 +#: documents/models.py:1221 paperless_mail/models.py:296 msgid "assign this correspondent" msgstr "" -#: documents/models.py:1227 +#: documents/models.py:1230 msgid "assign this storage path" msgstr "" -#: documents/models.py:1236 +#: documents/models.py:1239 msgid "assign this owner" msgstr "" -#: documents/models.py:1243 +#: documents/models.py:1246 msgid "grant view permissions to these users" msgstr "" -#: documents/models.py:1250 +#: documents/models.py:1253 msgid "grant view permissions to these groups" msgstr "" -#: documents/models.py:1257 +#: documents/models.py:1260 msgid "grant change permissions to these users" msgstr "" -#: documents/models.py:1264 +#: documents/models.py:1267 msgid "grant change permissions to these groups" msgstr "" -#: documents/models.py:1271 +#: documents/models.py:1274 msgid "assign these custom fields" msgstr "" -#: documents/models.py:1275 +#: documents/models.py:1278 msgid "custom field values" msgstr "" -#: documents/models.py:1279 +#: documents/models.py:1282 msgid "Optional values to assign to the custom fields." msgstr "" -#: documents/models.py:1288 +#: documents/models.py:1291 msgid "remove these tag(s)" msgstr "" -#: documents/models.py:1293 +#: documents/models.py:1296 msgid "remove all tags" msgstr "" -#: documents/models.py:1300 +#: documents/models.py:1303 msgid "remove these document type(s)" msgstr "" -#: documents/models.py:1305 +#: documents/models.py:1308 msgid "remove all document types" msgstr "" -#: documents/models.py:1312 +#: documents/models.py:1315 msgid "remove these correspondent(s)" msgstr "" -#: documents/models.py:1317 +#: documents/models.py:1320 msgid "remove all correspondents" msgstr "" -#: documents/models.py:1324 +#: documents/models.py:1327 msgid "remove these storage path(s)" msgstr "" -#: documents/models.py:1329 +#: documents/models.py:1332 msgid "remove all storage paths" msgstr "" -#: documents/models.py:1336 +#: documents/models.py:1339 msgid "remove these owner(s)" msgstr "" -#: documents/models.py:1341 +#: documents/models.py:1344 msgid "remove all owners" msgstr "" -#: documents/models.py:1348 +#: documents/models.py:1351 msgid "remove view permissions for these users" msgstr "" -#: documents/models.py:1355 +#: documents/models.py:1358 msgid "remove view permissions for these groups" msgstr "" -#: documents/models.py:1362 +#: documents/models.py:1365 msgid "remove change permissions for these users" msgstr "" -#: documents/models.py:1369 +#: documents/models.py:1372 msgid "remove change permissions for these groups" msgstr "" -#: documents/models.py:1374 +#: documents/models.py:1377 msgid "remove all permissions" msgstr "" -#: documents/models.py:1381 +#: documents/models.py:1384 msgid "remove these custom fields" msgstr "" -#: documents/models.py:1386 +#: documents/models.py:1389 msgid "remove all custom fields" msgstr "" -#: documents/models.py:1395 +#: documents/models.py:1398 msgid "email" msgstr "" -#: documents/models.py:1404 +#: documents/models.py:1407 msgid "webhook" msgstr "" -#: documents/models.py:1408 +#: documents/models.py:1411 msgid "workflow action" msgstr "" -#: documents/models.py:1409 +#: documents/models.py:1412 msgid "workflow actions" msgstr "" -#: documents/models.py:1418 paperless_mail/models.py:145 +#: documents/models.py:1421 paperless_mail/models.py:145 msgid "order" msgstr "" -#: documents/models.py:1424 +#: documents/models.py:1427 msgid "triggers" msgstr "" -#: documents/models.py:1431 +#: documents/models.py:1434 msgid "actions" msgstr "" -#: documents/models.py:1434 paperless_mail/models.py:154 +#: documents/models.py:1437 paperless_mail/models.py:154 msgid "enabled" msgstr "" -#: documents/models.py:1445 +#: documents/models.py:1448 msgid "workflow" msgstr "" -#: documents/models.py:1449 +#: documents/models.py:1452 msgid "workflow trigger type" msgstr "" -#: documents/models.py:1463 +#: documents/models.py:1466 msgid "date run" msgstr "" -#: documents/models.py:1469 +#: documents/models.py:1472 msgid "workflow run" msgstr "" -#: documents/models.py:1470 +#: documents/models.py:1473 msgid "workflow runs" msgstr "" -#: documents/serialisers.py:135 +#: documents/serialisers.py:134 #, python-format msgid "Invalid regular expression: %(error)s" msgstr "" -#: documents/serialisers.py:561 +#: documents/serialisers.py:560 msgid "Invalid color." msgstr "" -#: documents/serialisers.py:1614 +#: documents/serialisers.py:1609 #, python-format msgid "File type %(type)s not supported" msgstr "" From 6647b32ac06f011f9f3e5ccd924e127ae2ad7612 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 16 May 2025 17:30:34 -0700 Subject: [PATCH 3/7] Fix: ignore logo file from sanity checker (#9946) --- src/documents/sanity_checker.py | 5 +++++ src/documents/tests/test_sanity_check.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/documents/sanity_checker.py b/src/documents/sanity_checker.py index 6cef98f1a..4f7abde06 100644 --- a/src/documents/sanity_checker.py +++ b/src/documents/sanity_checker.py @@ -82,6 +82,11 @@ def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages: if lockfile in present_files: present_files.remove(lockfile) + if settings.APP_LOGO: + logo_file = Path(settings.MEDIA_ROOT / settings.APP_LOGO).resolve() + if logo_file in present_files: + present_files.remove(logo_file) + for doc in tqdm(Document.global_objects.all(), disable=not progress): # Check sanity of the thumbnail thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve() diff --git a/src/documents/tests/test_sanity_check.py b/src/documents/tests/test_sanity_check.py index 0dec2d53b..c33062c42 100644 --- a/src/documents/tests/test_sanity_check.py +++ b/src/documents/tests/test_sanity_check.py @@ -5,6 +5,7 @@ from pathlib import Path import filelock from django.conf import settings from django.test import TestCase +from django.test import override_settings from documents.models import Document from documents.sanity_checker import check_sanity @@ -157,6 +158,17 @@ class TestSanityCheck(DirectoriesMixin, TestCase): "Orphaned file in media dir", ) + @override_settings( + APP_LOGO="logo/logo.png", + ) + def test_ignore_logo(self): + self.make_test_data() + logo_dir = Path(self.dirs.media_dir, "logo") + logo_dir.mkdir(parents=True, exist_ok=True) + Path(self.dirs.media_dir, "logo", "logo.png").touch() + messages = check_sanity() + self.assertFalse(messages.has_warning) + def test_archive_filename_no_checksum(self): doc = self.make_test_data() doc.archive_checksum = None From 83db0355f3a51652900b29b113d770fd3fab1e5b Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 16 May 2025 22:21:24 -0700 Subject: [PATCH 4/7] Chore: automatically disable email verification if no smtp setup (#9949) --- docs/configuration.md | 2 +- src/paperless/settings.py | 46 +++++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1a0f15a6c..4196ffa8f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -629,7 +629,7 @@ If both the [PAPERLESS_ACCOUNT_DEFAULT_GROUPS](#PAPERLESS_ACCOUNT_DEFAULT_GROUPS !!! note - If you do not have a working email server set up you should set this to 'none'. + If you do not have a working email server set up this will be set to 'none'. #### [`PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS=`](#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS) {#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS} diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 4906b4713..3e6e76f11 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -462,6 +462,24 @@ CHANNEL_LAYERS = { }, } +############################################################################### +# Email (SMTP) Backend # +############################################################################### + +EMAIL_HOST: Final[str] = os.getenv("PAPERLESS_EMAIL_HOST", "localhost") +EMAIL_PORT: Final[int] = int(os.getenv("PAPERLESS_EMAIL_PORT", 25)) +EMAIL_HOST_USER: Final[str] = os.getenv("PAPERLESS_EMAIL_HOST_USER", "") +EMAIL_HOST_PASSWORD: Final[str] = os.getenv("PAPERLESS_EMAIL_HOST_PASSWORD", "") +DEFAULT_FROM_EMAIL: Final[str] = os.getenv("PAPERLESS_EMAIL_FROM", EMAIL_HOST_USER) +EMAIL_USE_TLS: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_TLS") +EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL") +EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] " +EMAIL_TIMEOUT = 30.0 +EMAIL_ENABLED = EMAIL_HOST != "localhost" or EMAIL_HOST_USER != "" +if DEBUG: # pragma: no cover + EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" + EMAIL_FILE_PATH = BASE_DIR / "sent_emails" + ############################################################################### # Security # ############################################################################### @@ -503,9 +521,13 @@ REDIRECT_LOGIN_TO_SSO = __get_boolean("PAPERLESS_REDIRECT_LOGIN_TO_SSO") AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME") -ACCOUNT_EMAIL_VERIFICATION = os.getenv( - "PAPERLESS_ACCOUNT_EMAIL_VERIFICATION", - "optional", +ACCOUNT_EMAIL_VERIFICATION = ( + "none" + if not EMAIL_ENABLED + else os.getenv( + "PAPERLESS_ACCOUNT_EMAIL_VERIFICATION", + "optional", + ) ) ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = __get_boolean( @@ -1220,24 +1242,6 @@ NLTK_ENABLED: Final[bool] = __get_boolean("PAPERLESS_ENABLE_NLTK", "yes") NLTK_LANGUAGE: str | None = _get_nltk_language_setting(OCR_LANGUAGE) -############################################################################### -# Email (SMTP) Backend # -############################################################################### - -EMAIL_HOST: Final[str] = os.getenv("PAPERLESS_EMAIL_HOST", "localhost") -EMAIL_PORT: Final[int] = int(os.getenv("PAPERLESS_EMAIL_PORT", 25)) -EMAIL_HOST_USER: Final[str] = os.getenv("PAPERLESS_EMAIL_HOST_USER", "") -EMAIL_HOST_PASSWORD: Final[str] = os.getenv("PAPERLESS_EMAIL_HOST_PASSWORD", "") -DEFAULT_FROM_EMAIL: Final[str] = os.getenv("PAPERLESS_EMAIL_FROM", EMAIL_HOST_USER) -EMAIL_USE_TLS: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_TLS") -EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL") -EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] " -EMAIL_TIMEOUT = 30.0 -EMAIL_ENABLED = EMAIL_HOST != "localhost" or EMAIL_HOST_USER != "" -if DEBUG: # pragma: no cover - EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" - EMAIL_FILE_PATH = BASE_DIR / "sent_emails" - ############################################################################### # Email Preprocessors # ############################################################################### From fae4116504a1a386ad9a6f3eaa29e67218f4be44 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 05:22:59 +0000 Subject: [PATCH 5/7] Auto translate strings --- src/locale/en_US/LC_MESSAGES/django.po | 70 +++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 9bc32b0c3..d53efa8a8 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-16 14:24+0000\n" +"POT-Creation-Date: 2025-05-17 05:22+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -1645,139 +1645,139 @@ msgstr "" msgid "paperless application settings" msgstr "" -#: paperless/settings.py:732 +#: paperless/settings.py:754 msgid "English (US)" msgstr "" -#: paperless/settings.py:733 +#: paperless/settings.py:755 msgid "Arabic" msgstr "" -#: paperless/settings.py:734 +#: paperless/settings.py:756 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:735 +#: paperless/settings.py:757 msgid "Belarusian" msgstr "" -#: paperless/settings.py:736 +#: paperless/settings.py:758 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:737 +#: paperless/settings.py:759 msgid "Catalan" msgstr "" -#: paperless/settings.py:738 +#: paperless/settings.py:760 msgid "Czech" msgstr "" -#: paperless/settings.py:739 +#: paperless/settings.py:761 msgid "Danish" msgstr "" -#: paperless/settings.py:740 +#: paperless/settings.py:762 msgid "German" msgstr "" -#: paperless/settings.py:741 +#: paperless/settings.py:763 msgid "Greek" msgstr "" -#: paperless/settings.py:742 +#: paperless/settings.py:764 msgid "English (GB)" msgstr "" -#: paperless/settings.py:743 +#: paperless/settings.py:765 msgid "Spanish" msgstr "" -#: paperless/settings.py:744 +#: paperless/settings.py:766 msgid "Finnish" msgstr "" -#: paperless/settings.py:745 +#: paperless/settings.py:767 msgid "French" msgstr "" -#: paperless/settings.py:746 +#: paperless/settings.py:768 msgid "Hungarian" msgstr "" -#: paperless/settings.py:747 +#: paperless/settings.py:769 msgid "Italian" msgstr "" -#: paperless/settings.py:748 +#: paperless/settings.py:770 msgid "Japanese" msgstr "" -#: paperless/settings.py:749 +#: paperless/settings.py:771 msgid "Korean" msgstr "" -#: paperless/settings.py:750 +#: paperless/settings.py:772 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:751 +#: paperless/settings.py:773 msgid "Norwegian" msgstr "" -#: paperless/settings.py:752 +#: paperless/settings.py:774 msgid "Dutch" msgstr "" -#: paperless/settings.py:753 +#: paperless/settings.py:775 msgid "Polish" msgstr "" -#: paperless/settings.py:754 +#: paperless/settings.py:776 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:755 +#: paperless/settings.py:777 msgid "Portuguese" msgstr "" -#: paperless/settings.py:756 +#: paperless/settings.py:778 msgid "Romanian" msgstr "" -#: paperless/settings.py:757 +#: paperless/settings.py:779 msgid "Russian" msgstr "" -#: paperless/settings.py:758 +#: paperless/settings.py:780 msgid "Slovak" msgstr "" -#: paperless/settings.py:759 +#: paperless/settings.py:781 msgid "Slovenian" msgstr "" -#: paperless/settings.py:760 +#: paperless/settings.py:782 msgid "Serbian" msgstr "" -#: paperless/settings.py:761 +#: paperless/settings.py:783 msgid "Swedish" msgstr "" -#: paperless/settings.py:762 +#: paperless/settings.py:784 msgid "Turkish" msgstr "" -#: paperless/settings.py:763 +#: paperless/settings.py:785 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:764 +#: paperless/settings.py:786 msgid "Chinese Simplified" msgstr "" -#: paperless/settings.py:765 +#: paperless/settings.py:787 msgid "Chinese Traditional" msgstr "" From 55917fcabef34a3cdf4ad7d03695cf6d84ae9f02 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 19 May 2025 09:38:01 -0700 Subject: [PATCH 6/7] Fix: handle created change with api version increment, use created only on frontend, deprecate created_date (#9962) --- docs/api.md | 6 ++++ .../saved-view-widget.component.html | 2 +- .../document-detail.component.html | 4 +-- .../document-detail.component.ts | 4 +-- .../document-card-large.component.html | 4 +-- .../document-card-small.component.html | 4 +-- .../document-list.component.html | 2 +- src-ui/src/app/data/document.ts | 3 -- .../src/app/services/rest/document.service.ts | 2 -- src-ui/src/environments/environment.prod.ts | 2 +- src/documents/serialisers.py | 23 +++++++++++++ src/documents/tests/test_api_documents.py | 32 +++++++++++++++++++ src/paperless/settings.py | 4 +-- 13 files changed, 74 insertions(+), 18 deletions(-) diff --git a/docs/api.md b/docs/api.md index 44c9a4bc4..ac2789f8b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -418,3 +418,9 @@ Initial API version. - The user field of document notes now returns a simplified user object rather than just the user ID. + +#### Version 9 + +- The document `created` field is now a date, not a datetime. The + `created_date` field is considered deprecated and will be removed in a + future version. diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index 305584a64..53fa86dd3 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -43,7 +43,7 @@ {{doc.added | customDate}} } @case (DisplayField.CREATED) { - {{doc.created_date | customDate}} + {{doc.created | customDate}} } @case (DisplayField.TITLE) { {{doc.title | documentTitle}} diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 19d6faabb..067246335 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -129,8 +129,8 @@
- +
- Created: {{ document.created_date | customDate }} + Created: {{ document.created | customDate }} Added: {{ document.added | customDate }} Modified: {{ document.modified | customDate }}
@if (displayFields.includes(DisplayField.CREATED)) {
- {{document.created_date | customDate:'mediumDate'}} + {{document.created | customDate:'mediumDate'}}
} @if (displayFields.includes(DisplayField.ADDED)) { diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index a166acd94..662bb9bab 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -73,14 +73,14 @@
- Created: {{ document.created_date | customDate }} + Created: {{ document.created | customDate }} Added: {{ document.added | customDate }} Modified: {{ document.modified | customDate }}
- {{document.created_date | customDate:'mediumDate'}} + {{document.created | customDate:'mediumDate'}}
} diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index e50805098..c58d1ede1 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -348,7 +348,7 @@ } @if (activeDisplayFields.includes(DisplayField.CREATED)) { - {{d.created_date | customDate}} + {{d.created | customDate}} } @if (activeDisplayFields.includes(DisplayField.ADDED)) { diff --git a/src-ui/src/app/data/document.ts b/src-ui/src/app/data/document.ts index e5f00148e..5c23a8600 100644 --- a/src-ui/src/app/data/document.ts +++ b/src-ui/src/app/data/document.ts @@ -130,9 +130,6 @@ export interface Document extends ObjectWithPermissions { // UTC created?: Date - // localized date - created_date?: Date - modified?: Date added?: Date diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 9cdb86280..1f67a358b 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -190,8 +190,6 @@ export class DocumentService extends AbstractPaperlessService { } patch(o: Document): Observable { - // we want to only set created_date - delete o.created o.remove_inbox_tags = !!this.settingsService.get( SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS ) diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 2bc42f4e9..a05991d9e 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI) export const environment = { production: true, apiBaseUrl: document.baseURI + 'api/', - apiVersion: '8', // match src/paperless/settings.py + apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', version: '2.15.3', webSocketHost: window.location.host, diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 062775544..9782932bc 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -21,6 +21,7 @@ from django.utils.crypto import get_random_string from django.utils.text import slugify from django.utils.translation import gettext as _ from drf_spectacular.utils import extend_schema_field +from drf_spectacular.utils import extend_schema_serializer from drf_writable_nested.serializers import NestedUpdateMixin from guardian.core import ObjectPermissionChecker from guardian.shortcuts import get_users_with_perms @@ -891,6 +892,9 @@ class NotesSerializer(serializers.ModelSerializer): return ret +@extend_schema_serializer( + deprecate_fields=["created_date"], +) class DocumentSerializer( OwnedObjectSerializer, NestedUpdateMixin, @@ -943,6 +947,22 @@ class DocumentSerializer( doc = super().to_representation(instance) if self.truncate_content and "content" in self.fields: doc["content"] = doc.get("content")[0:550] + + request = self.context.get("request") + api_version = int( + request.version if request else settings.REST_FRAMEWORK["DEFAULT_VERSION"], + ) + + if api_version < 9: + # provide created as a datetime for backwards compatibility + from django.utils import timezone + + doc["created"] = timezone.make_aware( + datetime.combine( + instance.created, + datetime.min.time(), + ), + ).isoformat() return doc def validate(self, attrs): @@ -968,6 +988,9 @@ class DocumentSerializer( instance.created = validated_data.get("created_date") instance.save() if "created_date" in validated_data: + logger.warning( + "created_date is deprecated, use created instead", + ) validated_data.pop("created_date") if instance.custom_fields.count() > 0 and "custom_fields" in validated_data: incoming_custom_fields = [ diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index c63ffdb57..bb77f5818 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -171,6 +171,38 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results[0]), 0) + def test_document_legacy_created_format(self): + """ + GIVEN: + - Existing document + WHEN: + - Document is requested with api version ≥ 9 + - Document is requested with api version < 9 + THEN: + - Document created field is returned as date + - Document created field is returned as datetime + """ + doc = Document.objects.create( + title="none", + checksum="123", + mime_type="application/pdf", + created=date(2023, 1, 1), + ) + + response = self.client.get( + f"/api/documents/{doc.pk}/", + headers={"Accept": "application/json; version=8"}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertRegex(response.data["created"], r"^2023-01-01T00:00:00.*$") + + response = self.client.get( + f"/api/documents/{doc.pk}/", + headers={"Accept": "application/json; version=9"}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["created"], "2023-01-01") + def test_document_update_with_created_date(self): """ GIVEN: diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 3e6e76f11..1eaf93920 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -342,10 +342,10 @@ REST_FRAMEWORK = { "rest_framework.authentication.SessionAuthentication", ], "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning", - "DEFAULT_VERSION": "8", # match src-ui/src/environments/environment.prod.ts + "DEFAULT_VERSION": "9", # match src-ui/src/environments/environment.prod.ts # Make sure these are ordered and that the most recent version appears # last. See api.md#api-versioning when adding new versions. - "ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7", "8"], + "ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7", "8", "9"], # DRF Spectacular default schema "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } From 71bc483eec9020555f809409555784d6b8aa8feb Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 16:41:14 +0000 Subject: [PATCH 7/7] Auto translate strings --- src-ui/messages.xlf | 2 +- src/locale/en_US/LC_MESSAGES/django.po | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index d302039dc..01e2d9fbb 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -7554,7 +7554,7 @@ - Created: + Created: src/app/components/document-list/document-card-large/document-card-large.component.html 115,116 diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index d53efa8a8..f556d33d1 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-17 05:22+0000\n" +"POT-Creation-Date: 2025-05-19 16:38+0000\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -1176,21 +1176,21 @@ msgstr "" msgid "workflow runs" msgstr "" -#: documents/serialisers.py:134 +#: documents/serialisers.py:135 #, python-format msgid "Invalid regular expression: %(error)s" msgstr "" -#: documents/serialisers.py:560 +#: documents/serialisers.py:561 msgid "Invalid color." msgstr "" -#: documents/serialisers.py:1609 +#: documents/serialisers.py:1632 #, python-format msgid "File type %(type)s not supported" msgstr "" -#: documents/serialisers.py:1703 +#: documents/serialisers.py:1726 msgid "Invalid variable detected." msgstr ""