Interviews attended: a no-show counts as held, and a held interview that is moved or re-settled counts twice #448

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

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the interview counters and settling interviews in a test database.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at applications/views.py:977-987 (InterviewUpdateView.form_valid); the other locations have not moved.

interviews_held() counts diary interviews marked held plus every interview timeline entry that has no held diary twin at the same application and moment. Settling an interview as nobody showed up writes an entry of that same kind, and nothing ties a settling entry to the interview it came from. So a no-show is counted as an interview attended, and a held interview that is later moved or given another outcome is counted twice. The figure is on the Interviews widget and on the report handed to an employment office ("Interviews attended").

  • Where:
    • applications/services.py:517-524, settle_interview: the no-show branch calls record_event(..., kind=EventKind.INTERVIEW, summary="Nobody showed up for the ...", occurred_at=interview.starts_at), the same kind the held branch writes at :496-503.
    • applications/analytics.py:247-261, interviews_held: the diary side is outcome=InterviewOutcome.DONE; the timeline side is every EventKind.INTERVIEW entry whose (application_id, occurred_at) is not in twins, the set of (application_id, starts_at) of held diary rows.
    • applications/analytics.py:264-287, _first_interview_days: reads the same entries, so a no-show also counts under Applications that reached one and in the median days to an interview.
    • applications/services.py:439-445, reschedule_interview: moves starts_at whatever the outcome, and the held entry keeps the old date. Reached from InterviewUpdateView.form_valid (applications/views.py:937-947) and PATCH /api/v1/interviews/{pk} (api/routers/interviews.py:153).
    • applications/services.py:488-490: a new outcome is written over an old one, and the earlier entry stays. Reached from POST /api/v1/interviews/{pk}/outcome and InterviewOutcomeView.
    • Shown by applications/reports.py:493 and templates/applications/report_print.html:137-138.
  • What happens (each checked on the snapshot):
    1. An upcoming video interview is settled as nobody showed up: interviews_held is 1, Insights.interviewed is 1, and the report's happened.interviews is 1.
    2. An interview entered after it happened is recorded as held straight away (services.py:410-411). Correcting it to nobody showed up gives 2: the held entry stays and the no-show entry is added, and neither has a held twin any more. Correcting it again to cancelled still gives 2.
    3. A held interview whose time is corrected from 10:00 to 11:00 goes from 1 to 2: the diary row moves, the entry does not, so both are counted.
  • Why it matters: the report exists to prove activity to an employment office, and it states interviews attended that did not take place. Insights and the report both overstate the rate of reaching an interview.
  • Fix: give a no-show its own event kind (as a cancellation already has INTERVIEW_CANCELLED), with a data migration that moves existing "Nobody showed up" entries, and keep it out of both counters. Link the settling entry to its interview (a nullable foreign key or the interview's uid on the event) and deduplicate on that rather than on the start time: an entry linked to an interview is counted through the diary row alone, so moving or re-settling the interview changes the count once and the log keeps what was written.

Related to #224 (closed), whose item 3 introduced the deduplication by start time that these cases escape.

Done when

  • tests/test_counting.py has a test that settles an interview as a no-show and expects interviews_held and the report's interviews to be 0 and Insights.interviewed to be 0.
  • A test there that moves a held interview by an hour expects interviews_held to stay at 1, and one that re-settles a held interview as a no-show expects 0.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the interview counters and settling interviews in a test database. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `applications/views.py:977-987` (`InterviewUpdateView.form_valid`); the other locations have not moved. `interviews_held()` counts diary interviews marked held plus every *interview* timeline entry that has no held diary twin at the same application and moment. Settling an interview as *nobody showed up* writes an entry of that same kind, and nothing ties a settling entry to the interview it came from. So a no-show is counted as an interview attended, and a held interview that is later moved or given another outcome is counted twice. The figure is on the Interviews widget and on the report handed to an employment office ("Interviews attended"). - **Where:** - `applications/services.py:517-524`, `settle_interview`: the no-show branch calls `record_event(..., kind=EventKind.INTERVIEW, summary="Nobody showed up for the ...", occurred_at=interview.starts_at)`, the same kind the held branch writes at `:496-503`. - `applications/analytics.py:247-261`, `interviews_held`: the diary side is `outcome=InterviewOutcome.DONE`; the timeline side is every `EventKind.INTERVIEW` entry whose `(application_id, occurred_at)` is not in `twins`, the set of `(application_id, starts_at)` of held diary rows. - `applications/analytics.py:264-287`, `_first_interview_days`: reads the same entries, so a no-show also counts under *Applications that reached one* and in the median days to an interview. - `applications/services.py:439-445`, `reschedule_interview`: moves `starts_at` whatever the outcome, and the held entry keeps the old date. Reached from `InterviewUpdateView.form_valid` (`applications/views.py:937-947`) and `PATCH /api/v1/interviews/{pk}` (`api/routers/interviews.py:153`). - `applications/services.py:488-490`: a new outcome is written over an old one, and the earlier entry stays. Reached from `POST /api/v1/interviews/{pk}/outcome` and `InterviewOutcomeView`. - Shown by `applications/reports.py:493` and `templates/applications/report_print.html:137-138`. - **What happens** (each checked on the snapshot): 1. An upcoming video interview is settled as *nobody showed up*: `interviews_held` is 1, `Insights.interviewed` is 1, and the report's `happened.interviews` is 1. 2. An interview entered after it happened is recorded as held straight away (`services.py:410-411`). Correcting it to *nobody showed up* gives 2: the held entry stays and the no-show entry is added, and neither has a held twin any more. Correcting it again to *cancelled* still gives 2. 3. A held interview whose time is corrected from 10:00 to 11:00 goes from 1 to 2: the diary row moves, the entry does not, so both are counted. - **Why it matters:** the report exists to prove activity to an employment office, and it states interviews attended that did not take place. Insights and the report both overstate the rate of reaching an interview. - **Fix:** give a no-show its own event kind (as a cancellation already has `INTERVIEW_CANCELLED`), with a data migration that moves existing "Nobody showed up" entries, and keep it out of both counters. Link the settling entry to its interview (a nullable foreign key or the interview's `uid` on the event) and deduplicate on that rather than on the start time: an entry linked to an interview is counted through the diary row alone, so moving or re-settling the interview changes the count once and the log keeps what was written. Related to #224 (closed), whose item 3 introduced the deduplication by start time that these cases escape. ## Done when - `tests/test_counting.py` has a test that settles an interview as a no-show and expects `interviews_held` and the report's interviews to be 0 and `Insights.interviewed` to be 0. - A test there that moves a held interview by an hour expects `interviews_held` to stay at 1, and one that re-settles a held interview as a no-show expects 0.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:46 +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#448
No description provided.