ClassificationGrouping not cleaned up when classifications are deleted (pre_delete recalc runs before delete)
- Dominant language
- Python
- Stars
- 30
- Forks
- 3
- Avg merge
- 9h 28m
- Merged PRs (30d)
- 42
Description
🤖 Written by Claude
## Symptom
After hard-deleting classifications, their `ClassificationGrouping` rows remain in the grouping grid with no linked classifications, and are never cleaned up (there is no nightly/scheduled sweep — grouping recalculation is entirely signal-driven).
## Cause
The delete handler in `classification/signals/classification_hooks_grouping.py` (`deleting_classification`) is on **`pre_delete`** and recalculates synchronously, *before* the classification row is actually deleted:
1. `pre_delete` fires; the handler calls `entry.dirty_up()` then `_instant_undirty_check()`.
2. `ClassificationGrouping.update()` runs right then — but the `ClassificationGroupingEntry` still exists (it is only removed by CASCADE when the classification row is deleted). So `classification_modifications` still includes the doomed classification, the grouping re-saves with `dirty=False`, and the "no classifications, time to die" branch (`ClassificationGrouping.update()`, `classification_grouping.py` ~line 469) never runs.
3. The delete commits, the entry cascades away, leaving a stale grouping with `dirty=False` that nothing ever revisits.
So the signal exists but is effectively a no-op for deletes — it recalculates the pre-delete state and then marks the grouping as up to date.
## Suggested fix
Keep the handler on `pre_delete` (by `post_delete` the entry has already been cascaded away and can't be found), but make the recalc not see the dying entry:
- In the handler, explicitly delete the `ClassificationGroupingEntry` and `dirty_up()` its grouping *before* calling `_instant_undirty_check()`, so the recount excludes the classification and empty groupings self-delete. This mirrors what `assign_grouping_for_classification` already does in its "no allele" branch.
- Alternatively, register `transaction.on_commit(ClassificationGrouping.update_all_dirty)` instead of recalculating inline — also safer if the surrounding delete transaction rolls back.
## Cleanup of existing orphans
`python3 manage.py classification_groupings --refresh` re-runs `update()` on every grouping, so empty ones delete themselves.
Note: `AlleleOriginGrouping.update()` never deletes itself when empty (its aggregation logic is mostly commented out with a FIX-ME), so empty allele-origin/allele grouping shells will still linger after the refresh.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.