fix: subset selection validation and remove unnecessary hash computation
Two bugs fixed in animal selection with checkboxes: 1. Confirmation dialog showed wrong count (e.g., "35 animals" instead of "2 animals" when only 2 were selected). Fixed by using valid_selected count in diff.server_count instead of full filter resolution count. 2. Spurious "Selection Changed" dialogs due to race condition in async hash computation. Fixed by removing client-side hash computation entirely - it was unnecessary since the server validates selected_ids directly against the filter resolution. Changes: - validation.py: Remove hash comparison in _validate_subset(), validate IDs directly, fix server_count in diff - animal_select.py: Remove computeSelectionHash(), hidden roster_hash field, and related async fetch code - test_selection_validation.py: Add tests for subset mode validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -185,39 +185,30 @@ def _validate_subset(
|
||||
# Find selected IDs that no longer match the filter
|
||||
invalid_ids = selected_set - resolved_set
|
||||
|
||||
# Compute valid selected IDs (those that still match the filter)
|
||||
valid_selected = [sid for sid in selected_ids if sid in resolved_set]
|
||||
|
||||
if not invalid_ids:
|
||||
# All selected IDs are valid - compute hash from selected IDs
|
||||
subset_hash = compute_roster_hash(selected_ids, context.from_location_id)
|
||||
|
||||
# Verify hash matches what client sent
|
||||
if subset_hash == context.roster_hash:
|
||||
return SelectionValidationResult(
|
||||
valid=True,
|
||||
resolved_ids=selected_ids,
|
||||
roster_hash=context.roster_hash,
|
||||
diff=None,
|
||||
)
|
||||
|
||||
# Some selected IDs are no longer valid, or hash mismatch
|
||||
# Compute diff: removed = invalid_ids, added = none
|
||||
diff = SelectionDiff(
|
||||
added=[],
|
||||
removed=sorted(invalid_ids),
|
||||
server_count=len(resolved_ids),
|
||||
client_count=len(selected_ids),
|
||||
)
|
||||
|
||||
if context.confirmed and not invalid_ids:
|
||||
# Client confirmed, and all IDs are still valid
|
||||
# All selected IDs are still in the filter resolution - valid
|
||||
# No hash comparison needed: we validate IDs directly
|
||||
return SelectionValidationResult(
|
||||
valid=True,
|
||||
resolved_ids=selected_ids,
|
||||
roster_hash=context.roster_hash,
|
||||
diff=diff,
|
||||
roster_hash=compute_roster_hash(selected_ids, context.from_location_id),
|
||||
diff=None,
|
||||
)
|
||||
|
||||
# Invalid - return with valid selected IDs (those that still match)
|
||||
valid_selected = [sid for sid in selected_ids if sid in resolved_set]
|
||||
# Some selected IDs are no longer valid
|
||||
# Compute diff: removed = invalid_ids, added = none
|
||||
# In subset mode, server_count reflects valid selected count, not full filter
|
||||
diff = SelectionDiff(
|
||||
added=[],
|
||||
removed=sorted(invalid_ids),
|
||||
server_count=len(valid_selected),
|
||||
client_count=len(selected_ids),
|
||||
)
|
||||
|
||||
# Return invalid with valid selected IDs (those that still match)
|
||||
new_hash = compute_roster_hash(valid_selected, context.from_location_id)
|
||||
|
||||
return SelectionValidationResult(
|
||||
|
||||
@@ -98,8 +98,6 @@ def animal_checkbox_list(
|
||||
*resolved_id_fields,
|
||||
# Hidden field to indicate subset selection mode
|
||||
Input(type="hidden", name="subset_mode", value="true"),
|
||||
# Hidden field for roster_hash - will be updated via JS
|
||||
Input(type="hidden", name="roster_hash", id="roster-hash-field"),
|
||||
# Script for selection management
|
||||
selection_script(len(animals)),
|
||||
id="animal-selection-list",
|
||||
@@ -126,8 +124,6 @@ def selection_script(total_count: int) -> Div:
|
||||
if (countText) {{
|
||||
countText.textContent = checked + ' of {total_count} selected';
|
||||
}}
|
||||
// Trigger hash recomputation if needed
|
||||
computeSelectionHash();
|
||||
}}
|
||||
|
||||
function selectAllAnimals(selectAll) {{
|
||||
@@ -137,50 +133,6 @@ def selection_script(total_count: int) -> Div:
|
||||
}});
|
||||
updateSelectionCount();
|
||||
}}
|
||||
|
||||
function getSelectedIds() {{
|
||||
var checkboxes = document.querySelectorAll('#animal-selection-list input[name="selected_ids"]:checked');
|
||||
return Array.from(checkboxes).map(cb => cb.value);
|
||||
}}
|
||||
|
||||
function computeSelectionHash() {{
|
||||
// Get selected IDs and compute hash via API
|
||||
var selectedIds = getSelectedIds();
|
||||
var fromLocationId = document.querySelector('input[name="from_location_id"]');
|
||||
var fromLoc = fromLocationId ? fromLocationId.value : '';
|
||||
|
||||
fetch('/api/compute-hash', {{
|
||||
method: 'POST',
|
||||
headers: {{'Content-Type': 'application/json'}},
|
||||
body: JSON.stringify({{
|
||||
selected_ids: selectedIds,
|
||||
from_location_id: fromLoc
|
||||
}})
|
||||
}})
|
||||
.then(response => {{
|
||||
if (!response.ok) {{
|
||||
console.error('Hash computation failed:', response.status);
|
||||
return {{}};
|
||||
}}
|
||||
var contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {{
|
||||
return response.json();
|
||||
}}
|
||||
return {{}};
|
||||
}})
|
||||
.then(data => {{
|
||||
var hashField = document.getElementById('roster-hash-field');
|
||||
if (hashField && data.roster_hash) {{
|
||||
hashField.value = data.roster_hash;
|
||||
}}
|
||||
}})
|
||||
.catch(err => console.error('Hash computation error:', err));
|
||||
}}
|
||||
|
||||
// Initialize hash on load
|
||||
document.addEventListener('DOMContentLoaded', function() {{
|
||||
computeSelectionHash();
|
||||
}});
|
||||
""")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user