Interviews booked through the API: the reminder names the hour in the server's zone #564

Closed
opened 2026-10-01 10:47:40 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, by following where the interview services get their time zone and language from, and booking an interview through the API with the test client.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at notifications/management/commands/send_due_reminders.py:85 (the summary sent as the title); the rest is where it was. The scheduled sync's side of this has since been filed as #335, so this issue keeps the API and the calendar feeds and shares the helper with it.

The interview services write the time of an interview into stored text: the reminder's summary, which is later sent word for word as the notification title, and the timeline entries "… scheduled for …" and "… moved from … to …". They format it in whatever zone and language are active when they run. Only a signed-in browser session activates the owner's zone and language. An API call and the scheduler's calendar sync do not, so the reminder can name the wrong hour for the interview.

  • Where:
    • applications/services.py:354-375: _when() and _interview_reminder_summary() format timezone.localtime(...) with DATETIME_FORMAT/TIME_FORMAT under the active language.
    • applications/services.py:414-435 (schedule_interview) and :446-472 (reschedule_interview) store those strings in ApplicationEvent.summary and Reminder.summary. :172-187 (change_status) stores "Old → New" the same way.
    • core/middleware.py:124-146 and :174-182: UserPreferencesMiddleware finds a profile only when request.user.is_authenticated. A bearer token never logs anyone in (api/auth.py:58-66), so an API call gets the instance default zone and the language LocaleMiddleware took from Accept-Language.
    • plugins/syncing.py:70-96: sync_connection runs the plugin with nothing activated. postulo-dav's sync.py:195 calls reschedule_interview, so the reminder is re-worded in settings.TIME_ZONE and LANGUAGE_CODE. That path is #335.
    • notifications/management/commands/send_due_reminders.py:85 sends reminder.summary as the title. notify() (notifications/service.py:49-55) switches the language, but it cannot change words that are already stored, and it does not switch the zone.
  • What happens: the profile is in America/New_York and French, and the instance is in Europe/Paris. An agent books a video interview through POST /api/v1/interviews for 15:00 UTC, which is 10:00 in New York. The reminder is stored as "Interview tomorrow: Video call at Aperture Science, 16:00", in English and in Paris time. The day before, the phone announces 16:00 for a 10:00 interview. The timeline reads "Video call scheduled for 2 Nov 2026, 16:00". Moving the event in the person's own calendar does the same through the CalDAV sync (#335). The /interviews/calendar.ics feed also words each SUMMARY (applications/ical.py:147) in the calendar client's Accept-Language.
  • Why it matters: a reminder that arrives at the right moment but names the wrong hour is worse than none. #224 item 4 fixed this for moves made in the browser. Its chosen fix, re-wording the text on every reschedule, re-creates the wrong hour whenever the move happens outside a session.
  • Fix:
    • Add one helper, for example core.context.acting_for(user), that enters timezone.override(profile.time_zone or site.default_time_zone()) and translation.override(notifications.service.language_for(user)).
    • Enter it in the API once the token has resolved (in TokenAuth/ScopedAuth.authenticate, or in a small wrapper around each operation) and in the ICS feeds. #335 asks for the same helper around plugins.syncing.sync_connection for connection.owner; whichever lands first adds it, and the other uses it.
    • Better still for the reminder, take #224's other option: keep the time out of the stored text and word it when it is shown or sent, inside notify's override, with the zone overridden too.

Related to #335 (open), which covers the same fault when a scheduled sync moves an interview, to #224 (closed), which made the reminder re-worded on reschedule, and to #223 (closed), which moved notification language to the recipient but did not cover text stored before delivery or the time zone.

Done when

  • tests/test_interviews.py: an interview booked through POST /api/v1/interviews for a profile in America/New_York has a reminder, and a timeline entry, that name 10:00 for a 15:00 UTC start while the instance zone is Europe/Paris.
  • A test of /interviews/calendar.ics for that profile asserts each SUMMARY is worded in the profile's language whatever Accept-Language the calendar client sends. (The sync pass has its test in #335.)
Found on 2026-09-29 in a read-only review of postulo at 95348908d, by following where the interview services get their time zone and language from, and booking an interview through the API with the test client. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `notifications/management/commands/send_due_reminders.py:85` (the summary sent as the title); the rest is where it was. The scheduled sync's side of this has since been filed as #335, so this issue keeps the API and the calendar feeds and shares the helper with it. The interview services write the time of an interview into stored text: the reminder's summary, which is later sent word for word as the notification title, and the timeline entries "… scheduled for …" and "… moved from … to …". They format it in whatever zone and language are active when they run. Only a signed-in browser session activates the owner's zone and language. An API call and the scheduler's calendar sync do not, so the reminder can name the wrong hour for the interview. - **Where:** - `applications/services.py:354-375`: `_when()` and `_interview_reminder_summary()` format `timezone.localtime(...)` with `DATETIME_FORMAT`/`TIME_FORMAT` under the active language. - `applications/services.py:414-435` (`schedule_interview`) and `:446-472` (`reschedule_interview`) store those strings in `ApplicationEvent.summary` and `Reminder.summary`. `:172-187` (`change_status`) stores "Old → New" the same way. - `core/middleware.py:124-146` and `:174-182`: `UserPreferencesMiddleware` finds a profile only when `request.user.is_authenticated`. A bearer token never logs anyone in (`api/auth.py:58-66`), so an API call gets the instance default zone and the language `LocaleMiddleware` took from `Accept-Language`. - `plugins/syncing.py:70-96`: `sync_connection` runs the plugin with nothing activated. postulo-dav's `sync.py:195` calls `reschedule_interview`, so the reminder is re-worded in `settings.TIME_ZONE` and `LANGUAGE_CODE`. That path is #335. - `notifications/management/commands/send_due_reminders.py:85` sends `reminder.summary` as the title. `notify()` (`notifications/service.py:49-55`) switches the language, but it cannot change words that are already stored, and it does not switch the zone. - **What happens:** the profile is in `America/New_York` and French, and the instance is in `Europe/Paris`. An agent books a video interview through `POST /api/v1/interviews` for 15:00 UTC, which is 10:00 in New York. The reminder is stored as "Interview tomorrow: Video call at Aperture Science, 16:00", in English and in Paris time. The day before, the phone announces 16:00 for a 10:00 interview. The timeline reads "Video call scheduled for 2 Nov 2026, 16:00". Moving the event in the person's own calendar does the same through the CalDAV sync (#335). The `/interviews/calendar.ics` feed also words each `SUMMARY` (`applications/ical.py:147`) in the calendar client's `Accept-Language`. - **Why it matters:** a reminder that arrives at the right moment but names the wrong hour is worse than none. #224 item 4 fixed this for moves made in the browser. Its chosen fix, re-wording the text on every reschedule, re-creates the wrong hour whenever the move happens outside a session. - **Fix:** - Add one helper, for example `core.context.acting_for(user)`, that enters `timezone.override(profile.time_zone or site.default_time_zone())` and `translation.override(notifications.service.language_for(user))`. - Enter it in the API once the token has resolved (in `TokenAuth`/`ScopedAuth.authenticate`, or in a small wrapper around each operation) and in the ICS feeds. #335 asks for the same helper around `plugins.syncing.sync_connection` for `connection.owner`; whichever lands first adds it, and the other uses it. - Better still for the reminder, take #224's other option: keep the time out of the stored text and word it when it is shown or sent, inside `notify`'s override, with the zone overridden too. Related to #335 (open), which covers the same fault when a scheduled sync moves an interview, to #224 (closed), which made the reminder re-worded on reschedule, and to #223 (closed), which moved notification language to the recipient but did not cover text stored before delivery or the time zone. ## Done when - `tests/test_interviews.py`: an interview booked through `POST /api/v1/interviews` for a profile in `America/New_York` has a reminder, and a timeline entry, that name 10:00 for a 15:00 UTC start while the instance zone is `Europe/Paris`. - A test of `/interviews/calendar.ics` for that profile asserts each `SUMMARY` is worded in the profile's language whatever `Accept-Language` the calendar client sends. (The sync pass has its test in #335.)
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:40 +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#564
No description provided.