Compare commits

...

8 Commits

34 changed files with 646 additions and 469 deletions

View File

@ -418,3 +418,9 @@ Initial API version.
- The user field of document notes now returns a simplified user object - The user field of document notes now returns a simplified user object
rather than just the user ID. 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.

View File

@ -629,7 +629,7 @@ If both the [PAPERLESS_ACCOUNT_DEFAULT_GROUPS](#PAPERLESS_ACCOUNT_DEFAULT_GROUPS
!!! note !!! 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=<bool>`](#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS) {#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS} #### [`PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS=<bool>`](#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS) {#PAPERLESS_ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS}

View File

@ -7554,7 +7554,7 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3727324658595204357" datatype="html"> <trans-unit id="3727324658595204357" datatype="html">
<source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created_date | customDate }}"/></source> <source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created | customDate }}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">115,116</context> <context context-type="linenumber">115,116</context>

View File

@ -43,7 +43,7 @@
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a> <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
} }
@case (DisplayField.CREATED) { @case (DisplayField.CREATED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a> <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created | customDate}}</a>
} }
@case (DisplayField.TITLE) { @case (DisplayField.TITLE) {
<a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a> <a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>

View File

@ -129,8 +129,8 @@
<div> <div>
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text> <pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number> <pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
<pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" <pngx-input-date i18n-title title="Date created" formControlName="created" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error?.created_date"></pngx-input-date> [error]="error?.created"></pngx-input-date>
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)" <pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)"
(createNew)="createCorrespondent($event)" [hideAddButton]="createDisabled(DataType.Correspondent)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select> (createNew)="createCorrespondent($event)" [hideAddButton]="createDisabled(DataType.Correspondent)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
<pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)" <pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)"

View File

@ -208,7 +208,7 @@ export class DocumentDetailComponent
documentForm: FormGroup = new FormGroup({ documentForm: FormGroup = new FormGroup({
title: new FormControl(''), title: new FormControl(''),
content: new FormControl(''), content: new FormControl(''),
created_date: new FormControl(), created: new FormControl(),
correspondent: new FormControl(), correspondent: new FormControl(),
document_type: new FormControl(), document_type: new FormControl(),
storage_path: new FormControl(), storage_path: new FormControl(),
@ -490,7 +490,7 @@ export class DocumentDetailComponent
this.store = new BehaviorSubject({ this.store = new BehaviorSubject({
title: doc.title, title: doc.title,
content: doc.content, content: doc.content,
created_date: doc.created_date, created: doc.created,
correspondent: doc.correspondent, correspondent: doc.correspondent,
document_type: doc.document_type, document_type: doc.document_type,
storage_path: doc.storage_path, storage_path: doc.storage_path,

View File

@ -112,14 +112,14 @@
@if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) { @if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) {
<ng-template #dateTooltip> <ng-template #dateTooltip>
<div class="d-flex flex-column text-light"> <div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created_date | customDate }}</span> <span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span> <span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span> <span i18n>Modified: {{ document.modified | customDate }}</span>
</div> </div>
</ng-template> </ng-template>
@if (displayFields.includes(DisplayField.CREATED)) { @if (displayFields.includes(DisplayField.CREATED)) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip"> <div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small> <i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created | customDate:'mediumDate'}}</small>
</div> </div>
} }
@if (displayFields.includes(DisplayField.ADDED)) { @if (displayFields.includes(DisplayField.ADDED)) {

View File

@ -73,14 +73,14 @@
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateTooltip> <ng-template #dateTooltip>
<div class="d-flex flex-column text-light"> <div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created_date | customDate }}</span> <span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span> <span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span> <span i18n>Modified: {{ document.modified | customDate }}</span>
</div> </div>
</ng-template> </ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip"> <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs> <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
<small>{{document.created_date | customDate:'mediumDate'}}</small> <small>{{document.created | customDate:'mediumDate'}}</small>
</div> </div>
</div> </div>
} }

View File

@ -348,7 +348,7 @@
} }
@if (activeDisplayFields.includes(DisplayField.CREATED)) { @if (activeDisplayFields.includes(DisplayField.CREATED)) {
<td> <td>
{{d.created_date | customDate}} {{d.created | customDate}}
</td> </td>
} }
@if (activeDisplayFields.includes(DisplayField.ADDED)) { @if (activeDisplayFields.includes(DisplayField.ADDED)) {

View File

@ -130,9 +130,6 @@ export interface Document extends ObjectWithPermissions {
// UTC // UTC
created?: Date created?: Date
// localized date
created_date?: Date
modified?: Date modified?: Date
added?: Date added?: Date

View File

@ -190,8 +190,6 @@ export class DocumentService extends AbstractPaperlessService<Document> {
} }
patch(o: Document): Observable<Document> { patch(o: Document): Observable<Document> {
// we want to only set created_date
delete o.created
o.remove_inbox_tags = !!this.settingsService.get( o.remove_inbox_tags = !!this.settingsService.get(
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
) )

View File

@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI)
export const environment = { export const environment = {
production: true, production: true,
apiBaseUrl: document.baseURI + 'api/', apiBaseUrl: document.baseURI + 'api/',
apiVersion: '8', // match src/paperless/settings.py apiVersion: '9', // match src/paperless/settings.py
appTitle: 'Paperless-ngx', appTitle: 'Paperless-ngx',
version: '2.15.3', version: '2.15.3',
webSocketHost: window.location.host, webSocketHost: window.location.host,

View File

@ -48,6 +48,15 @@ CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
ID_KWARGS = ["in", "exact"] ID_KWARGS = ["in", "exact"]
INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"] INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
DATE_KWARGS = [ DATE_KWARGS = [
"year",
"month",
"day",
"gt",
"gte",
"lt",
"lte",
]
DATETIME_KWARGS = [
"year", "year",
"month", "month",
"day", "day",
@ -731,6 +740,19 @@ class DocumentFilterSet(FilterSet):
mime_type = MimeTypeFilter() 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: class Meta:
model = Document model = Document
fields = { fields = {
@ -739,8 +761,8 @@ class DocumentFilterSet(FilterSet):
"content": CHAR_KWARGS, "content": CHAR_KWARGS,
"archive_serial_number": INT_KWARGS, "archive_serial_number": INT_KWARGS,
"created": DATE_KWARGS, "created": DATE_KWARGS,
"added": DATE_KWARGS, "added": DATETIME_KWARGS,
"modified": DATE_KWARGS, "modified": DATETIME_KWARGS,
"original_filename": CHAR_KWARGS, "original_filename": CHAR_KWARGS,
"checksum": CHAR_KWARGS, "checksum": CHAR_KWARGS,
"correspondent": ["isnull"], "correspondent": ["isnull"],
@ -764,8 +786,8 @@ class ShareLinkFilterSet(FilterSet):
class Meta: class Meta:
model = ShareLink model = ShareLink
fields = { fields = {
"created": DATE_KWARGS, "created": DATETIME_KWARGS,
"expiration": DATE_KWARGS, "expiration": DATETIME_KWARGS,
} }

View File

@ -5,6 +5,7 @@ import math
from collections import Counter from collections import Counter
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime from datetime import datetime
from datetime import time
from datetime import timezone from datetime import timezone
from shutil import rmtree from shutil import rmtree
from typing import TYPE_CHECKING 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=doc.document_type.name if doc.document_type else None,
type_id=doc.document_type.id 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, has_type=doc.document_type is not None,
created=doc.created, created=datetime.combine(doc.created, time.min),
added=doc.added, added=doc.added,
asn=asn, asn=asn,
modified=doc.modified, modified=doc.modified,

View File

@ -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",
),
),
]

View File

@ -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 = models.DateTimeField(
_("modified"), _("modified"),
@ -291,8 +295,7 @@ class Document(SoftDeleteModel, ModelWithOwner):
verbose_name_plural = _("documents") verbose_name_plural = _("documents")
def __str__(self) -> str: def __str__(self) -> str:
# Convert UTC database time to local time created = self.created.isoformat()
created = datetime.date.isoformat(timezone.localdate(self.created))
res = f"{created}" res = f"{created}"
@ -371,7 +374,7 @@ class Document(SoftDeleteModel, ModelWithOwner):
@property @property
def created_date(self): def created_date(self):
return timezone.localdate(self.created) return self.created
class SavedView(ModelWithOwner): class SavedView(ModelWithOwner):

View File

@ -82,6 +82,11 @@ def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages:
if lockfile in present_files: if lockfile in present_files:
present_files.remove(lockfile) 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): for doc in tqdm(Document.global_objects.all(), disable=not progress):
# Check sanity of the thumbnail # Check sanity of the thumbnail
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve() thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve()

View File

@ -1,10 +1,9 @@
from __future__ import annotations from __future__ import annotations
import datetime
import logging import logging
import math import math
import re import re
import zoneinfo from datetime import datetime
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -22,6 +21,7 @@ from django.utils.crypto import get_random_string
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from drf_spectacular.utils import extend_schema_serializer
from drf_writable_nested.serializers import NestedUpdateMixin from drf_writable_nested.serializers import NestedUpdateMixin
from guardian.core import ObjectPermissionChecker from guardian.core import ObjectPermissionChecker
from guardian.shortcuts import get_users_with_perms from guardian.shortcuts import get_users_with_perms
@ -423,7 +423,7 @@ class OwnedObjectListSerializer(serializers.ListSerializer):
class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
last_correspondence = serializers.DateTimeField(read_only=True, required=False) last_correspondence = serializers.DateField(read_only=True, required=False)
class Meta: class Meta:
model = Correspondent model = Correspondent
@ -892,6 +892,9 @@ class NotesSerializer(serializers.ModelSerializer):
return ret return ret
@extend_schema_serializer(
deprecate_fields=["created_date"],
)
class DocumentSerializer( class DocumentSerializer(
OwnedObjectSerializer, OwnedObjectSerializer,
NestedUpdateMixin, NestedUpdateMixin,
@ -944,6 +947,22 @@ class DocumentSerializer(
doc = super().to_representation(instance) doc = super().to_representation(instance)
if self.truncate_content and "content" in self.fields: if self.truncate_content and "content" in self.fields:
doc["content"] = doc.get("content")[0:550] 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 return doc
def validate(self, attrs): def validate(self, attrs):
@ -966,13 +985,12 @@ class DocumentSerializer(
def update(self, instance: Document, validated_data): def update(self, instance: Document, validated_data):
if "created_date" in validated_data and "created" not in validated_data: if "created_date" in validated_data and "created" not in validated_data:
new_datetime = datetime.datetime.combine( instance.created = validated_data.get("created_date")
validated_data.get("created_date"),
datetime.time(0, 0, 0, 0, zoneinfo.ZoneInfo(settings.TIME_ZONE)),
)
instance.created = new_datetime
instance.save() instance.save()
if "created_date" in validated_data: if "created_date" in validated_data:
logger.warning(
"created_date is deprecated, use created instead",
)
validated_data.pop("created_date") validated_data.pop("created_date")
if instance.custom_fields.count() > 0 and "custom_fields" in validated_data: if instance.custom_fields.count() > 0 and "custom_fields" in validated_data:
incoming_custom_fields = [ incoming_custom_fields = [
@ -1646,6 +1664,11 @@ class PostDocumentSerializer(serializers.Serializer):
else: else:
return None 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): class BulkDownloadSerializer(DocumentListSerializer):
content = serializers.ChoiceField( content = serializers.ChoiceField(

View File

@ -722,7 +722,7 @@ def run_workflows(
timezone.localtime(document.added), timezone.localtime(document.added),
document.original_filename or "", document.original_filename or "",
document.filename or "", document.filename or "",
timezone.localtime(document.created), document.created,
) )
except Exception: except Exception:
logger.exception( logger.exception(
@ -1010,7 +1010,7 @@ def run_workflows(
filename = document.original_filename or "" filename = document.original_filename or ""
current_filename = document.filename or "" current_filename = document.filename or ""
added = timezone.localtime(document.added) added = timezone.localtime(document.added)
created = timezone.localtime(document.created) created = document.created
else: else:
title = overrides.title if overrides.title else str(document.original_file) title = overrides.title if overrides.title else str(document.original_file)
doc_url = "" doc_url = ""
@ -1032,7 +1032,7 @@ def run_workflows(
filename = document.original_file if document.original_file else "" filename = document.original_file if document.original_file else ""
current_filename = filename current_filename = filename
added = timezone.localtime(timezone.now()) added = timezone.localtime(timezone.now())
created = timezone.localtime(overrides.created) created = overrides.created
subject = ( subject = (
parse_w_workflow_placeholders( parse_w_workflow_placeholders(
@ -1098,7 +1098,7 @@ def run_workflows(
filename = document.original_filename or "" filename = document.original_filename or ""
current_filename = document.filename or "" current_filename = document.filename or ""
added = timezone.localtime(document.added) added = timezone.localtime(document.added)
created = timezone.localtime(document.created) created = document.created
else: else:
title = overrides.title if overrides.title else str(document.original_file) title = overrides.title if overrides.title else str(document.original_file)
doc_url = "" doc_url = ""
@ -1120,7 +1120,7 @@ def run_workflows(
filename = document.original_file if document.original_file else "" filename = document.original_file if document.original_file else ""
current_filename = filename current_filename = filename
added = timezone.localtime(timezone.now()) added = timezone.localtime(timezone.now())
created = timezone.localtime(overrides.created) created = overrides.created
try: try:
data = {} data = {}

View File

@ -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 Given a Document, localizes the creation date and builds a context dictionary with some common, shorthand
formatted values from it formatted values from it
""" """
local_created = timezone.localdate(document.created)
return { return {
"created": local_created.isoformat(), "created": document.created.isoformat(),
"created_year": local_created.strftime("%Y"), "created_year": document.created.strftime("%Y"),
"created_year_short": local_created.strftime("%y"), "created_year_short": document.created.strftime("%y"),
"created_month": local_created.strftime("%m"), "created_month": document.created.strftime("%m"),
"created_month_name": local_created.strftime("%B"), "created_month_name": document.created.strftime("%B"),
"created_month_name_short": local_created.strftime("%b"), "created_month_name_short": document.created.strftime("%b"),
"created_day": local_created.strftime("%d"), "created_day": document.created.strftime("%d"),
} }

View File

@ -1,3 +1,4 @@
from datetime import date
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@ -10,7 +11,7 @@ def parse_w_workflow_placeholders(
local_added: datetime, local_added: datetime,
original_filename: str, original_filename: str,
filename: str, filename: str,
created: datetime | None = None, created: date | None = None,
doc_title: str | None = None, doc_title: str | None = None,
doc_url: str | None = None, doc_url: str | None = None,
) -> str: ) -> str:

View File

@ -171,6 +171,64 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
results = response.data["results"] results = response.data["results"]
self.assertEqual(len(results[0]), 0) 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:
- 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): def test_document_actions(self):
_, filename = tempfile.mkstemp(dir=self.dirs.originals_dir) _, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
@ -1313,7 +1371,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
_, overrides = self.get_last_consume_delay_call_args() _, 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): def test_upload_with_asn(self):
self.consume_file_mock.return_value = celery.result.AsyncResult( self.consume_file_mock.return_value = celery.result.AsyncResult(

View File

@ -4,7 +4,6 @@ from unittest import mock
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -104,13 +103,13 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
Document.objects.create( Document.objects.create(
mime_type="application/pdf", mime_type="application/pdf",
correspondent=self.c1, correspondent=self.c1,
created=timezone.make_aware(datetime.datetime(2022, 1, 1)), created=datetime.date(2022, 1, 1),
checksum="123", checksum="123",
) )
Document.objects.create( Document.objects.create(
mime_type="application/pdf", mime_type="application/pdf",
correspondent=self.c1, correspondent=self.c1,
created=timezone.make_aware(datetime.datetime(2022, 1, 2)), created=datetime.date(2022, 1, 2),
checksum="456", checksum="456",
) )

View File

@ -474,7 +474,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.client.force_authenticate(user1) self.client.force_authenticate(user1)
response = self.client.get( response = self.client.get(
"/api/documents/", "/api/documents/?ordering=-id",
format="json", format="json",
) )

View File

@ -721,7 +721,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
d3.tags.add(t2) d3.tags.add(t2)
d4 = Document.objects.create( d4 = Document.objects.create(
checksum="4", checksum="4",
created=timezone.make_aware(datetime.datetime(2020, 7, 13)), created=datetime.date(2020, 7, 13),
content="test", content="test",
original_filename="doc4.pdf", original_filename="doc4.pdf",
) )

View File

@ -1,3 +1,5 @@
from datetime import date
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
@ -116,6 +118,7 @@ class TestTrashAPI(APITestCase):
checksum="checksum", checksum="checksum",
mime_type="application/pdf", mime_type="application/pdf",
owner=self.user, owner=self.user,
created=date(2023, 1, 1),
) )
document_u1.delete() document_u1.delete()
document_not_owned = Document.objects.create( document_not_owned = Document.objects.create(
@ -123,6 +126,7 @@ class TestTrashAPI(APITestCase):
content="content2", content="content2",
checksum="checksum2", checksum="checksum2",
mime_type="application/pdf", mime_type="application/pdf",
created=date(2023, 1, 2),
) )
document_not_owned.delete() document_not_owned.delete()
user2 = User.objects.create_user(username="user2") user2 = User.objects.create_user(username="user2")

View File

@ -1,4 +1,5 @@
import shutil import shutil
from datetime import date
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
@ -39,18 +40,24 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
self.dt2 = DocumentType.objects.create(name="dt2") self.dt2 = DocumentType.objects.create(name="dt2")
self.t1 = Tag.objects.create(name="t1") self.t1 = Tag.objects.create(name="t1")
self.t2 = Tag.objects.create(name="t2") 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( self.doc2 = Document.objects.create(
checksum="B", checksum="B",
title="B", title="B",
correspondent=self.c1, correspondent=self.c1,
document_type=self.dt1, document_type=self.dt1,
created=date(2023, 1, 2),
) )
self.doc3 = Document.objects.create( self.doc3 = Document.objects.create(
checksum="C", checksum="C",
title="C", title="C",
correspondent=self.c2, correspondent=self.c2,
document_type=self.dt2, document_type=self.dt2,
created=date(2023, 1, 3),
) )
self.doc4 = Document.objects.create(checksum="D", title="D") self.doc4 = Document.objects.create(checksum="D", title="D")
self.doc5 = Document.objects.create(checksum="E", title="E") self.doc5 = Document.objects.create(checksum="E", title="E")
@ -529,6 +536,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
filename=sample2, filename=sample2,
mime_type="application/pdf", mime_type="application/pdf",
page_count=8, page_count=8,
created=date(2023, 1, 2),
) )
self.doc2.archive_filename = sample2_archive self.doc2.archive_filename = sample2_archive
self.doc2.save() self.doc2.save()
@ -557,6 +565,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
title="D", title="D",
filename=img_doc, filename=img_doc,
mime_type="image/jpeg", mime_type="image/jpeg",
created=date(2023, 1, 3),
) )
self.img_doc.archive_filename = img_doc_archive self.img_doc.archive_filename = img_doc_archive
self.img_doc.save() self.img_doc.save()

View File

@ -3,12 +3,10 @@ import os
import shutil import shutil
import stat import stat
import tempfile import tempfile
import zoneinfo
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
from unittest.mock import MagicMock from unittest.mock import MagicMock
from dateutil import tz
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -247,20 +245,9 @@ class TestConsumer(
self._assert_first_last_send_progress() self._assert_first_last_send_progress()
# Convert UTC time from DB to local time self.assertEqual(document.created.year, rough_create_date_local.year)
document_date_local = timezone.localtime(document.created) self.assertEqual(document.created.month, rough_create_date_local.month)
self.assertEqual(document.created.day, rough_create_date_local.day)
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
@override_settings(FILENAME_FORMAT=None) @override_settings(FILENAME_FORMAT=None)
def testDeleteMacFiles(self): def testDeleteMacFiles(self):
@ -931,7 +918,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase):
self.assertEqual( self.assertEqual(
document.created, document.created,
datetime.datetime(1996, 2, 20, tzinfo=tz.gettz(settings.TIME_ZONE)), datetime.date(1996, 2, 20),
) )
@override_settings(FILENAME_DATE_ORDER="YMD") @override_settings(FILENAME_DATE_ORDER="YMD")
@ -961,7 +948,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase):
self.assertEqual( self.assertEqual(
document.created, 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): def test_consume_date_filename_date_use_content(self):
@ -991,7 +978,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase):
self.assertEqual( self.assertEqual(
document.created, document.created,
datetime.datetime(1996, 2, 20, tzinfo=tz.gettz(settings.TIME_ZONE)), datetime.date(1996, 2, 20),
) )
@override_settings( @override_settings(
@ -1023,7 +1010,7 @@ class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase):
self.assertEqual( self.assertEqual(
document.created, document.created,
datetime.datetime(1997, 2, 20, tzinfo=tz.gettz(settings.TIME_ZONE)), datetime.date(1997, 2, 20),
) )

View File

@ -1,12 +1,11 @@
import shutil import shutil
import tempfile import tempfile
import zoneinfo from datetime import date
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
from django.test import TestCase from django.test import TestCase
from django.test import override_settings from django.test import override_settings
from django.utils import timezone
from documents.models import Correspondent from documents.models import Correspondent
from documents.models import Document from documents.models import Document
@ -81,60 +80,15 @@ class TestDocument(TestCase):
doc = Document( doc = Document(
mime_type="application/pdf", mime_type="application/pdf",
title="test", 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") 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): def test_file_name_jpg(self):
doc = Document( doc = Document(
mime_type="image/jpeg", mime_type="image/jpeg",
title="test", 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") self.assertEqual(doc.get_public_filename(), "2020-12-25 test.jpg")
@ -142,7 +96,7 @@ class TestDocument(TestCase):
doc = Document( doc = Document(
mime_type="application/zip", mime_type="application/zip",
title="test", 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") self.assertEqual(doc.get_public_filename(), "2020-12-25 test.zip")
@ -150,6 +104,6 @@ class TestDocument(TestCase):
doc = Document( doc = Document(
mime_type="image/jpegasd", mime_type="image/jpegasd",
title="test", 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") self.assertEqual(doc.get_public_filename(), "2020-12-25 test")

View File

@ -333,7 +333,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(generate_filename(doc1), "2020-03-06.pdf") 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") self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
@ -912,7 +912,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
def test_date(self): def test_date(self):
doc = Document.objects.create( doc = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -930,7 +930,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc = Document.objects.create( doc = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -951,7 +951,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc = Document.objects.create( doc = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -979,7 +979,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
) )
doc = Document.objects.create( doc = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -1007,7 +1007,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -1019,7 +1019,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
) )
doc_b = Document.objects.create( doc_b = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=5, pk=5,
checksum="abcde", checksum="abcde",
@ -1047,7 +1047,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -1055,7 +1055,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
) )
doc_b = Document.objects.create( doc_b = Document.objects.create(
title="does not matter", 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", mime_type="application/pdf",
pk=5, pk=5,
checksum="abcde", checksum="abcde",
@ -1074,9 +1074,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
def test_short_names_created(self): def test_short_names_created(self):
doc = Document.objects.create( doc = Document.objects.create(
title="The Title", title="The Title",
created=timezone.make_aware( created=datetime.date(1989, 12, 2),
datetime.datetime(1989, 12, 21, 7, 36, 51, 153),
),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,
checksum="2", checksum="2",
@ -1236,7 +1234,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="Does Matter", 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)), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,
@ -1302,7 +1300,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="Does Matter", 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)), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,
@ -1337,7 +1335,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="Does Matter", 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)), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,
@ -1369,7 +1367,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="Some Title", 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)), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,
@ -1474,7 +1472,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc_a = Document.objects.create( doc_a = Document.objects.create(
title="Some Title", 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)), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,
@ -1529,7 +1527,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
""" """
doc = Document.objects.create( doc = Document.objects.create(
title="Some Title! With @ Special # Characters", 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)), added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)),
mime_type="application/pdf", mime_type="application/pdf",
pk=2, pk=2,

View File

@ -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())

View File

@ -5,6 +5,7 @@ from pathlib import Path
import filelock import filelock
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test import override_settings
from documents.models import Document from documents.models import Document
from documents.sanity_checker import check_sanity from documents.sanity_checker import check_sanity
@ -157,6 +158,17 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
"Orphaned file in media dir", "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): def test_archive_filename_no_checksum(self):
doc = self.make_test_data() doc = self.make_test_data()
doc.archive_checksum = None doc.archive_checksum = None

File diff suppressed because it is too large Load Diff

View File

@ -342,10 +342,10 @@ REST_FRAMEWORK = {
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
], ],
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning", "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 # Make sure these are ordered and that the most recent version appears
# last. See api.md#api-versioning when adding new versions. # 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 # DRF Spectacular default schema
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
} }
@ -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 # # 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") AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME")
ACCOUNT_EMAIL_VERIFICATION = os.getenv( ACCOUNT_EMAIL_VERIFICATION = (
"PAPERLESS_ACCOUNT_EMAIL_VERIFICATION", "none"
"optional", if not EMAIL_ENABLED
else os.getenv(
"PAPERLESS_ACCOUNT_EMAIL_VERIFICATION",
"optional",
)
) )
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = __get_boolean( 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) 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 # # Email Preprocessors #
############################################################################### ###############################################################################