Background worker: errands run in the server's language and time zone, not their owner's #383

Closed
opened 2026-10-01 10:46:18 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, following what errands.perform sets up before it calls a handler, with and without POSTULO_BACKGROUND_WORK, and running a report_pdf errand the way the worker does, with nothing activated.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at core/site.py:207-210 (default_time_zone), applications/views.py:1239 (ReportPDFView.post), and in documents/rendering.py at :429 (the {{ date }} placeholder), :536-549 (sent_to, called at :646 and :709) and :682-684 (snapshot_report); every other location below is unchanged.

Related to #335, which reports the same defect for a scheduled sync and asks for one "as this person" helper; this issue is the errand side of it and should use that helper. Related to #340, whose point 5 proposes a chosen language for the report "whoever draws it"; it does not cover the time zone or the other handlers. #223 (closed) made notifications and documents follow the recipient's language, but predates the worker (#247), and the errand path does not use the helper it introduced.

UserPreferencesMiddleware activates the signed-in person's language and time zone for every request. An errand done in the request inherits them. An errand done by manage.py work runs with neither, so it words everything in LANGUAGE_CODE (en-gb) and computes every date in settings.TIME_ZONE (POSTULO_TIME_ZONE, Europe/Paris by default), ignoring the person's profile and the defaults an administrator chose in Server settings. A filed report can cover the wrong period and miss applications, a frozen letter can carry the wrong day, and the errand page and the timeline are written in a language the person did not choose. How the operator deployed the instance decides all of it.

  • Where:
    • core/errands.py:116-151 (perform): work.run(errand) is wrapped in logs.request_scope only; no translation.override, no timezone.override. core/tasks.py:19-23 (perform_errand) and core/management/commands/work.py activate nothing either.
    • core/middleware.py:123-143 (UserPreferencesMiddleware.__call__): the only place the person's zone (Profile.time_zone, else site.default_time_zone(), core/site.py:245-248) and language are activated, per request.
    • notifications/service.py:18-30 (language_for) and :49-50: the notifier handler (notifications/slow.py:46-54) already works round the gap inside notify. No other handler does.
    • The sections below name what each symptom touches.

1. The filed report covers the server's period and is worded in English

  • Where: applications/slow.py:32-39 (render_a_report); applications/views.py:1199 (ReportPDFView.post sends the raw query string, not a resolved period); applications/reports.py:198 (period_from: today = today or timezone.localdate()), :114-129 (Period.bounds, which uses timezone.get_current_timezone()), :434, :453, :460 and :477 (localdate and localtime for the evidence rows and Produced on); documents/rendering.py:708-710 (snapshot_report records translation.get_language()).
  • What happens: the instance's environment says Europe/Paris, the worker profile of docker/compose.yml is on, and a person reads Postulo in French with the profile zone America/New_York.
    1. At 22:30 on 30 September they open the report with no query string (the month in progress) and press the button that files it. In Paris it is already 1 October, so the worker files October's report.
    2. With ?on=2026-09, an application sent at 22:30 that evening is on the page and missing from the filed PDF, because Period.bounds puts the end of September at Paris midnight. Produced on and the evidence dates are Paris dates. Running the errand as the worker does gave exactly this: en-gb, the title "Job search report · September 2026", and no row for the application, while reports.build under the person's zone listed it.
    3. The labels, the month names and the title are in LANGUAGE_CODE, and snapshot_report records the document as English.
    4. The same applies when the administrator set the default zone or language in Server settings and left the environment alone: the worker never reads Server settings.
  • Why it matters: the PDF is filed under Sent documents as the record of what was handed to an employment office. It can state a different period, or different figures, from the page the person checked before pressing the button, in another language, and nothing warns them.

2. A frozen letter carries the server's date

  • Where: documents/slow.py:57-132 (freeze_what_was_sent), the only path that freezes a letter, through snapshot_letter; documents/rendering.py:455 (the {{ date }} placeholder: formats.date_format(timezone.localdate(), "DATE_FORMAT")); templates/documents/themes/base_letter.html:37 ({% now "j F Y" %}).
  • What happens: the same person records what they sent at 22:30 local time, with a letter. The preview they checked was drawn in the request and is dated 30 September. The worker draws the PDF in Paris time, so the letter handed to the employer and frozen as the record is dated 1 October. The month is still named in the letter's language, because render_letter_html overrides the language; only the day is wrong.
  • Why it matters: the date is printed on a document sent to an employer, and it disagrees with the preview and with the timeline entry for the same send.

3. Stored sentences and timeline text are in LANGUAGE_CODE

  • Where:
    • core/errands.py:130, :140 (error=str(refusal), where a lazy Refused(_(...)) is resolved) and :148.
    • Handlers that resolve their sentences when they run: core/slow.py:64; documents/slow.py:40, :48-52, :81 and :130; jobs/slow.py:66, :90, :116, :124, :140, :149 and :153; applications/slow.py:39 and :51-53.
    • core/models.py:775-780 (Errand.message returns the stored string) and templates/core/partials/errand_state.html:24 and :34, which print it.
    • documents/slow.py:123-127: record_event(summary=str(_("Documents sent"))).
    • documents/rendering.py:562-575 (sent_to), called at :672 and :735 outside the document-language override.
  • What happens: the person exports their data, captures a page, files the report or records what they sent. The errand page has a French heading, because working_label is resolved when the page renders, and under it "Your data is ready to download.", "Recorded what you sent." or "Nothing resembling a job posting was found on that page." in English. The timeline entry "Documents sent" is written in English and stays so, although the catalogues have it. RenderedDocument.sent_to stores " at " in English, where the same action without a worker stores it in the person's language. CV and letter bodies are not affected: documents/rendering.py overrides to the document's language (#223).
  • Why it matters: every person whose language is not en-gb meets this on every slow action once the operator turns the worker on, and part of it is written into a timeline they keep for months. It breaks the module's own promise, in the docstring of core/errands.py, that "there is no path here that only runs on somebody else's machine".

4. The comments and the wiki say otherwise

  • applications/slow.py:35-37 says the language "is the worker's, which is the instance default". It is LANGUAGE_CODE, whatever Server settings → Defaults says.
  • documents/rendering.py:708-710 says the language recorded for a report "is the request's (#283)". With a worker there is no request.
  • The wiki's Files and what you sent (Files-and-what-you-sent.md:72-73 in postulo.wiki at de1f52467) says a report is kept "in the language you were reading Postulo in when you pressed Download". With a worker it is not.

Fix

  • Move the middleware's resolution into one helper in core: the profile's zone, else site.default_time_zone(), else the environment, with an unknown zone name falling back rather than raising; the profile's language when the instance still offers it, else site.default_language(). language_for in notifications/service.py moves there too. UserPreferencesMiddleware, notify and the sync of #335 use the same helper.
  • In errands.perform, run work.run(errand) and every _finish sentence, including the one at line 130, inside translation.override(...) and timezone.override(...) for errand.owner. The inline path and the queued path then agree, and the report follows its owner until #340 gives it a language of its own.
  • In ReportPDFView.post, put the resolved period in the payload (reports.as_query(reports.period_from(request.GET))), so that neither the worker's zone nor a queue that crosses midnight chooses it.
  • Correct the two comments. The wiki sentence becomes true again without a change.
  • The scheduler's documents/archiving.py:108 (send_copy), which stores last_error sentences per owner with nothing activated, can use the same helper.

Done when

  • tests/test_errands.py: a handler that returns a translated sentence, and one that raises a lazy Refused, run through errands.perform with nothing activated for an owner whose profile language is French; the stored message and error are the French ones. It fails today.
  • tests/test_slow_work.py: a report_pdf errand performed with nothing activated, for a profile in America/New_York reading French, at a moment when that zone and settings.TIME_ZONE are on different dates, files a report in French with the period and the evidence rows the report page shows that person.
  • tests/test_errands.py: a sent_documents errand performed the same way, with the clock frozen where the two zones differ by a day, freezes a letter whose {{ date }} and {% now %} line are the owner's local date, and writes the event summary and sent_to in the owner's language.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, following what `errands.perform` sets up before it calls a handler, with and without `POSTULO_BACKGROUND_WORK`, and running a `report_pdf` errand the way the worker does, with nothing activated. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `core/site.py:207-210` (`default_time_zone`), `applications/views.py:1239` (`ReportPDFView.post`), and in `documents/rendering.py` at `:429` (the `{{ date }}` placeholder), `:536-549` (`sent_to`, called at `:646` and `:709`) and `:682-684` (`snapshot_report`); every other location below is unchanged. Related to #335, which reports the same defect for a scheduled sync and asks for one "as this person" helper; this issue is the errand side of it and should use that helper. Related to #340, whose point 5 proposes a chosen language for the report "whoever draws it"; it does not cover the time zone or the other handlers. #223 (closed) made notifications and documents follow the recipient's language, but predates the worker (#247), and the errand path does not use the helper it introduced. `UserPreferencesMiddleware` activates the signed-in person's language and time zone for every request. An errand done in the request inherits them. An errand done by `manage.py work` runs with neither, so it words everything in `LANGUAGE_CODE` (`en-gb`) and computes every date in `settings.TIME_ZONE` (`POSTULO_TIME_ZONE`, `Europe/Paris` by default), ignoring the person's profile and the defaults an administrator chose in Server settings. A filed report can cover the wrong period and miss applications, a frozen letter can carry the wrong day, and the errand page and the timeline are written in a language the person did not choose. How the operator deployed the instance decides all of it. - **Where:** - `core/errands.py:116-151` (`perform`): `work.run(errand)` is wrapped in `logs.request_scope` only; no `translation.override`, no `timezone.override`. `core/tasks.py:19-23` (`perform_errand`) and `core/management/commands/work.py` activate nothing either. - `core/middleware.py:123-143` (`UserPreferencesMiddleware.__call__`): the only place the person's zone (`Profile.time_zone`, else `site.default_time_zone()`, `core/site.py:245-248`) and language are activated, per request. - `notifications/service.py:18-30` (`language_for`) and `:49-50`: the notifier handler (`notifications/slow.py:46-54`) already works round the gap inside `notify`. No other handler does. - The sections below name what each symptom touches. ## 1. The filed report covers the server's period and is worded in English - **Where:** `applications/slow.py:32-39` (`render_a_report`); `applications/views.py:1199` (`ReportPDFView.post` sends the raw query string, not a resolved period); `applications/reports.py:198` (`period_from`: `today = today or timezone.localdate()`), `:114-129` (`Period.bounds`, which uses `timezone.get_current_timezone()`), `:434`, `:453`, `:460` and `:477` (`localdate` and `localtime` for the evidence rows and *Produced on*); `documents/rendering.py:708-710` (`snapshot_report` records `translation.get_language()`). - **What happens:** the instance's environment says `Europe/Paris`, the worker profile of `docker/compose.yml` is on, and a person reads Postulo in French with the profile zone `America/New_York`. 1. At 22:30 on 30 September they open the report with no query string (the month in progress) and press the button that files it. In Paris it is already 1 October, so the worker files October's report. 2. With `?on=2026-09`, an application sent at 22:30 that evening is on the page and missing from the filed PDF, because `Period.bounds` puts the end of September at Paris midnight. *Produced on* and the evidence dates are Paris dates. Running the errand as the worker does gave exactly this: `en-gb`, the title "Job search report · September 2026", and no row for the application, while `reports.build` under the person's zone listed it. 3. The labels, the month names and the title are in `LANGUAGE_CODE`, and `snapshot_report` records the document as English. 4. The same applies when the administrator set the default zone or language in Server settings and left the environment alone: the worker never reads Server settings. - **Why it matters:** the PDF is filed under *Sent documents* as the record of what was handed to an employment office. It can state a different period, or different figures, from the page the person checked before pressing the button, in another language, and nothing warns them. ## 2. A frozen letter carries the server's date - **Where:** `documents/slow.py:57-132` (`freeze_what_was_sent`), the only path that freezes a letter, through `snapshot_letter`; `documents/rendering.py:455` (the `{{ date }}` placeholder: `formats.date_format(timezone.localdate(), "DATE_FORMAT")`); `templates/documents/themes/base_letter.html:37` (`{% now "j F Y" %}`). - **What happens:** the same person records what they sent at 22:30 local time, with a letter. The preview they checked was drawn in the request and is dated 30 September. The worker draws the PDF in Paris time, so the letter handed to the employer and frozen as the record is dated 1 October. The month is still named in the letter's language, because `render_letter_html` overrides the language; only the day is wrong. - **Why it matters:** the date is printed on a document sent to an employer, and it disagrees with the preview and with the timeline entry for the same send. ## 3. Stored sentences and timeline text are in `LANGUAGE_CODE` - **Where:** - `core/errands.py:130`, `:140` (`error=str(refusal)`, where a lazy `Refused(_(...))` is resolved) and `:148`. - Handlers that resolve their sentences when they run: `core/slow.py:64`; `documents/slow.py:40`, `:48-52`, `:81` and `:130`; `jobs/slow.py:66`, `:90`, `:116`, `:124`, `:140`, `:149` and `:153`; `applications/slow.py:39` and `:51-53`. - `core/models.py:775-780` (`Errand.message` returns the stored string) and `templates/core/partials/errand_state.html:24` and `:34`, which print it. - `documents/slow.py:123-127`: `record_event(summary=str(_("Documents sent")))`. - `documents/rendering.py:562-575` (`sent_to`), called at `:672` and `:735` outside the document-language override. - **What happens:** the person exports their data, captures a page, files the report or records what they sent. The errand page has a French heading, because `working_label` is resolved when the page renders, and under it "Your data is ready to download.", "Recorded what you sent." or "Nothing resembling a job posting was found on that page." in English. The timeline entry "Documents sent" is written in English and stays so, although the catalogues have it. `RenderedDocument.sent_to` stores "<role> at <company>" in English, where the same action without a worker stores it in the person's language. CV and letter bodies are not affected: `documents/rendering.py` overrides to the document's language (#223). - **Why it matters:** every person whose language is not `en-gb` meets this on every slow action once the operator turns the worker on, and part of it is written into a timeline they keep for months. It breaks the module's own promise, in the docstring of `core/errands.py`, that "there is no path here that only runs on somebody else's machine". ## 4. The comments and the wiki say otherwise - `applications/slow.py:35-37` says the language "is the worker's, which is the instance default". It is `LANGUAGE_CODE`, whatever Server settings → Defaults says. - `documents/rendering.py:708-710` says the language recorded for a report "is the request's (#283)". With a worker there is no request. - The wiki's *Files and what you sent* (`Files-and-what-you-sent.md:72-73` in postulo.wiki at de1f52467) says a report is kept "in the language you were reading Postulo in when you pressed *Download*". With a worker it is not. ## Fix - Move the middleware's resolution into one helper in `core`: the profile's zone, else `site.default_time_zone()`, else the environment, with an unknown zone name falling back rather than raising; the profile's language when the instance still offers it, else `site.default_language()`. `language_for` in `notifications/service.py` moves there too. `UserPreferencesMiddleware`, `notify` and the sync of #335 use the same helper. - In `errands.perform`, run `work.run(errand)` and every `_finish` sentence, including the one at line 130, inside `translation.override(...)` and `timezone.override(...)` for `errand.owner`. The inline path and the queued path then agree, and the report follows its owner until #340 gives it a language of its own. - In `ReportPDFView.post`, put the resolved period in the payload (`reports.as_query(reports.period_from(request.GET))`), so that neither the worker's zone nor a queue that crosses midnight chooses it. - Correct the two comments. The wiki sentence becomes true again without a change. - The scheduler's `documents/archiving.py:108` (`send_copy`), which stores `last_error` sentences per owner with nothing activated, can use the same helper. ## Done when - `tests/test_errands.py`: a handler that returns a translated sentence, and one that raises a lazy `Refused`, run through `errands.perform` with nothing activated for an owner whose profile language is French; the stored message and error are the French ones. It fails today. - `tests/test_slow_work.py`: a `report_pdf` errand performed with nothing activated, for a profile in `America/New_York` reading French, at a moment when that zone and `settings.TIME_ZONE` are on different dates, files a report in French with the period and the evidence rows the report page shows that person. - `tests/test_errands.py`: a `sent_documents` errand performed the same way, with the clock frozen where the two zones differ by a day, freezes a letter whose `{{ date }}` and `{% now %}` line are the owner's local date, and writes the event summary and `sent_to` in the owner's language.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#383
No description provided.