Interviews attended: a no-show counts as held, and a held interview that is moved or re-settled counts twice #448
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#448
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, reading the interview counters and settling interviews in a test database.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atapplications/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").applications/services.py:517-524,settle_interview: the no-show branch callsrecord_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 isoutcome=InterviewOutcome.DONE; the timeline side is everyEventKind.INTERVIEWentry whose(application_id, occurred_at)is not intwins, 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: movesstarts_atwhatever the outcome, and the held entry keeps the old date. Reached fromInterviewUpdateView.form_valid(applications/views.py:937-947) andPATCH /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 fromPOST /api/v1/interviews/{pk}/outcomeandInterviewOutcomeView.applications/reports.py:493andtemplates/applications/report_print.html:137-138.interviews_heldis 1,Insights.interviewedis 1, and the report'shappened.interviewsis 1.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.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'suidon 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.pyhas a test that settles an interview as a no-show and expectsinterviews_heldand the report's interviews to be 0 andInsights.interviewedto be 0.interviews_heldto stay at 1, and one that re-settles a held interview as a no-show expects 0.