Verdicts drive the score: rejection excludes the contact, and is reversible #17
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/verdict-drives-score"
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?
Owner follow-up on the verdict work in #16: the two dropdowns were bookkeeping only - the points calculation did not follow, and a rejected contact could not be approved back. Both fixed.
1. A rejection adjusts the calculation
Per-QSO findings now point at their contact: every per-QSO flag in the pure rules engine carries
qso_index(MODE_NOT_ALLOWED, OUT_OF_TIME, locator flags, missing-field; DUP_BAND becomes one flag per excess logging, pointing at exactly that occurrence), the ingest persists it asflag.qso_id, and a verdict on such a flag excludes that contact from the score. The recomputation is not a second score model: it runs the samemonthly_scoreengine over the surviving contacts, re-derives the multiplier marks over the survivors (a rejected row cannot hoard a multiplier), and updates the cached entry fields the public pages read.Numbers on the live stack, corpus 17 freshly ingested: score 9 (3 x 3) → reject the DG flag → 4 (2 x 2 over the survivors) → approve it back → 9. Both changes audited with the toelichting as the reason.
2. Reversible
/manager/qso/{id}/verdictreaches the same mechanic from the row itself: a Goedgekeurd on an excluded contact restores it; a rejections chip (Afgekeurd + toelichting) shows on the row, controls only in the manager's drill-in (entrant views get the chip), and every change is one audit row. The flag verdict and the row verdict move the same boolean, so there is one source of truth.Boundaries
Migrations 0011 (
qso.excluded_by_manager) + 0012 (qso.verdict_note), both applied and verified on the live Postgres. Tests: 318.Notes: flags created before this change (existing entries) carry no contact link - their verdicts stay bookkeeping until a re-upload regenerates the log (flags are rebuilt per version).
The manager's entry drill-in showed no mode, so the mode could not be controlled there at all. The shared per-QSO table (preview, confirmation, manager drill-in and the live sessions view) gets a Mode column fed by a display helper: - The mode is rendered in the reglement's own vocabulary: FM, SSB, CW. SSB's emission labels USB/LSB are ACCEPTED and read as "SSB" (owner: do not disapprove them), the same normalisation the flag layer and the live logger apply - so a logged USB is an ordinary SSB row, no finding. - EDI's numeric mode codes never reach the table (1/2/6 are translated at parse time); a mode outside the reglement keeps its RAW value ("DG", "RY") with a MODE_NOT_ALLOWED marker on that row, so the manager can adjudicate what was actually logged. - A blank mode renders as an em dash - EDI allows an empty mode column by the standard, so absence is not a finding. Verified on the running stack: entry 1 (PD1SCO) shows FM/SSB per row; entry 4 (PD9MODE) keeps DG and RY with the marker. Tests: 312 (5 new).Owner follow-up on the verdict dropdown: rejecting an attention point must adjust the score automatically (it did nothing to the calculation), and a rejected contact must be approvable again - the manager is human. - app/rules/flags.py: every per-QSO flag now carries qso_index (the row it is about) in the pure computation; DUP_BAND becomes one flag per excess logging and points exactly at that occurrence, so a verdict rejects the dupe instead of the vague pair. INELIGIBLE_BAND and the section-shape flags stay entry-level. - ingest: Flag rows persist qso_id (flushed before the flag build, both on the merge and first-submit paths; the flush sits inside the IntegrityError guard because the entry insert happens there - caught by the race test). - Migration 0011 qso.excluded_by_manager + 0012 qso.verdict_note. - app/persistence/adjudicate.py: recompute_entry_score runs the SAME monthly_score engine over the surviving contacts (excluded rows score nothing, multiplier marks are re-derived over the survivors - a rejected row cannot hoard a multiplier); apply_flag_verdict / apply_qso_verdict set the verdict, recompute, and audit once, with the toelichting as the reason. - Routes: flag verdicts follow through to the contact; a per-contact verdict route /manager/qso/{id}/verdict approves or rejects from the row itself; the drill-in has the controls and chips (entrant views show chips only); a field edit now also recomputes, since it changes the score's basis. - Deliberately NOT in scope: entry-level flags (shape of the log, INELIGIBLE_BAND) keep their verdict as bookkeeping; consequences stay on the manual score tool. Verified on the running stack with a fresh ingest of corpus 17 (flags linked to their contacts): reject the DG flag -> score 9 -> 4 (2 points x 2 multipliers over the survivors); approve it back from the row -> 9 restored; both verdicts audited. Tests: 318.Owner rule: the Contest Manager may change a QSO (e.g. assign the correct band to a wrong-band contact) through the regular edit path, and every change names its reason - nothing may be modified silently. - /qso/{id}/edit: reason required server-side (empty -> back with ?reden=1 showing the notice; no field change, no audit row) + required in the form. - /entry/{id}/score (adjust score) and /entry/{id}/reset-pin: the same rule. - /entry/{id}/override: was required in the template, now enforced server-side too, so the browser can not be the only guard. - i18n: manager_reason_required (nl + en); the notice renders on ?reden=1. The verdicts are untouched: a verdict is itself the explicit decision (dropdown), its note stays optional. Tests: 325.