Dates of a moment taken in UTC: the day a store files a sent document under, and the retention report's #386
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#386
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, following aware datetimes to the places where they become a day.Checked again on 2026-10-01 against
mainat0a5cad587: still present.Two places turn a stored moment into a day with
.date(). WithUSE_TZon, the moment comes back from the database in UTC, so the day is UTC's, not the person's. A CV sent late in the evening or just after midnight is filed in the archive under the wrong day, and the retention report names the wrong day a contact was recorded, beside a cutoff that is worked out in local time.1. The day a store files a sent document under
documents/stores.py:84and:100,metadata_for:when = getattr(document, "archived_at", None) or document.created_atandsent_on=when.date() if origin == "render" and application is not None else None. In postulo-paperless,src/postulo_paperless/store.py:101(compose_title) and:318(put) usemetadata.sent_on or metadata.created_at.date()for the title and Paperless'screatedfield.CV — Company — Role — 2026-10-02. One rendered at 21:00 on 2 October in New York is filed under 3 October..date()on an aware datetime ignores the active zone, so it makes no difference thatmetadata_foralso runs in the scheduler (documents/archiving.py:136), where no zone is active anyway. An upload has nosent_on, and the plugin falls back tocreated_at.date(), which has the same fault on its side.2. The retention report's "recorded" date
Where:
core/gdpr.py:346(retention_dry_run: the cutoff istimezone.make_aware(dt.datetime.combine(cutoff, dt.time.min)), local midnight) againstcore/gdpr.py:354("created_at": contact.created_at.date().isoformat(), the UTC day) and:367("cutoff": cutoff.isoformat()); shown attemplates/server/data_protection.html:48-50and:58.What happens: a contact recorded at 00:30 local time is listed as recorded the previous day. Both dates also reach the page as ISO strings, so an administrator reading in German sees "recorded 2025-08-31" and "Records older than 2025-09-29" instead of dates in
DATE_FORMAT. The template lint added by #225 looks at|dateformats and cannot see a date that arrives as a string.Fix: in
metadata_for, take the day in the owner's zone:timezone.localtime(when, ZoneInfo(owner.profile.time_zone or site.default_time_zone())).date(), and give uploads a local date too (for example acreated_onfield onDocumentMetadata), so a store never has to call.date()on an aware datetime. Have postulo-paperless use that date. Inretention_dry_run, returntimezone.localdate(contact.created_at)andcutoffasdateobjects and format them in the template with|date:"DATE_FORMAT".Done when
tests/test_stores.pyhas a test that renders a document at 22:30 UTC for an owner inEurope/Parisand assertsmetadata_for(render).sent_onis the next day; the existing assertionmetadata.sent_on == render.rendered_at.date()changes to the local day.tests/test_gdpr.pyhas a test with a contact created at 23:30 UTC under an activeEurope/Pariszone, asserting the report's date is the local one and adateobject.