Insights, the report and salaries count the wrong things: quiet after interviews, withdrawals waiting, interviews missed, currencies mixed #224

Closed
opened 2026-09-15 21:23:24 +00:00 by tiagoagueda · 0 comments
Owner

Found in the 2026-09-15 code audit. Each item was checked by reading the code.

1. "Gone quiet" fires the day after an interview that has no outcome yet

  • quiet() requires next_interview_at to be null (applications/models.py:169-172).
  • with_next_interview counts only interviews with ends_at__gte=Now() (:180-189), so an interview waiting for its outcome is no longer "planned".
  • Last activity is the latest event, and scheduling writes its event dated at booking time (services.py:267-277).
  • An interview booked three weeks ahead therefore makes the application "quiet for 21+ days" the morning after, and the notifier announces it (quiet.py:58-92).
  • with_next_interview also ignores its at argument.

Fix: treat any interview whose outcome is still SCHEDULED as planned, including past ones, or fold the interview's ends_at into last activity.

2. Withdrawn applications count as "waiting on a reply" forever

analytics.py:254-261 excludes only GHOSTED, so a withdrawal with no reply counts as waiting and appears on the Outcomes widget.

Fix: restrict the count to status in QUIET_STATUSES, and add a test next to tests/test_insights.py:155-162.

3. The report and Insights disagree on interviews

  • The report's "interviews attended" counts only diary interviews marked held (reports.py:448-453).
  • Insights counts timeline interview entries (analytics.py:179-202), and the wiki promises a typed-in interview "counts as much as one settled from the diary".
  • Somebody who logs interviews on the timeline gets 0 on the document handed to the employment office.

Fix: count EventKind.INTERVIEW events by occurred_at inside the period, deduplicated against diary interviews.

4. A rescheduled interview's reminder keeps the old time

The reminder text includes the time when created (services.py:281-293), and reschedule_interview moves due_at but not the text (:321-327).

Fix: regenerate the text on reschedule, or build it from the interview when shown.

5. Salaries

CSV import (core/csv_import.py):

  • The currency is hard-coded to EUR (:813) while $ and £ are stripped (:601).
  • "50-60k" imports as 50 to 60,000, because the k is applied per side (:600, 614-618).
  • The split pattern matches a bare a anywhere (:614).
  • The period is never set, so "15 €/h" is stored as per year.

Display and sorting:

  • The salary column sorts on raw salary_max (applications/tables.py:83-88), so €30 an hour sorts below €40,000 a year and USD mixes with EUR.
  • salary_display leaves out the period (jobs/models.py:751-774).
  • The currency is a free three-character field with no validation.

Fix:

  • Import: detect currency symbols and ISO codes (with a per-file default on the mapping page), carry a trailing k to both sides, detect /h, /month and pa, and split on a only as a word.
  • Show the period.
  • Sort by an annualised amount within a currency, or by currency first.
  • Validate and upper-case ISO 4217 codes.
Found in the 2026-09-15 code audit. Each item was checked by reading the code. ## 1. "Gone quiet" fires the day after an interview that has no outcome yet - `quiet()` requires `next_interview_at` to be null (`applications/models.py:169-172`). - `with_next_interview` counts only interviews with `ends_at__gte=Now()` (`:180-189`), so an interview waiting for its outcome is no longer "planned". - Last activity is the latest event, and scheduling writes its event dated at booking time (`services.py:267-277`). - An interview booked three weeks ahead therefore makes the application "quiet for 21+ days" the morning after, and the notifier announces it (`quiet.py:58-92`). - `with_next_interview` also ignores its `at` argument. **Fix:** treat any interview whose outcome is still SCHEDULED as planned, including past ones, or fold the interview's `ends_at` into last activity. ## 2. Withdrawn applications count as "waiting on a reply" forever `analytics.py:254-261` excludes only GHOSTED, so a withdrawal with no reply counts as waiting and appears on the Outcomes widget. **Fix:** restrict the count to `status in QUIET_STATUSES`, and add a test next to `tests/test_insights.py:155-162`. ## 3. The report and Insights disagree on interviews - The report's "interviews attended" counts only diary interviews marked held (`reports.py:448-453`). - Insights counts timeline *interview* entries (`analytics.py:179-202`), and the wiki promises a typed-in interview "counts as much as one settled from the diary". - Somebody who logs interviews on the timeline gets 0 on the document handed to the employment office. **Fix:** count `EventKind.INTERVIEW` events by `occurred_at` inside the period, deduplicated against diary interviews. ## 4. A rescheduled interview's reminder keeps the old time The reminder text includes the time when created (`services.py:281-293`), and `reschedule_interview` moves `due_at` but not the text (`:321-327`). **Fix:** regenerate the text on reschedule, or build it from the interview when shown. ## 5. Salaries **CSV import** (`core/csv_import.py`): - The currency is hard-coded to EUR (`:813`) while `$` and `£` are stripped (`:601`). - `"50-60k"` imports as 50 to 60,000, because the `k` is applied per side (`:600, 614-618`). - The split pattern matches a bare `a` anywhere (`:614`). - The period is never set, so "15 €/h" is stored as per year. **Display and sorting:** - The salary column sorts on raw `salary_max` (`applications/tables.py:83-88`), so €30 an hour sorts below €40,000 a year and USD mixes with EUR. - `salary_display` leaves out the period (`jobs/models.py:751-774`). - The currency is a free three-character field with no validation. **Fix:** - Import: detect currency symbols and ISO codes (with a per-file default on the mapping page), carry a trailing `k` to both sides, detect `/h`, `/month` and `pa`, and split on `a` only as a word. - Show the period. - Sort by an annualised amount within a currency, or by currency first. - Validate and upper-case ISO 4217 codes.
tiagoagueda added this to the 0.4.0 milestone 2026-09-15 21:33:23 +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#224
No description provided.