diff --git a/src/animaltrack/web/templates/actions.py b/src/animaltrack/web/templates/actions.py index 27608b9..38890bd 100644 --- a/src/animaltrack/web/templates/actions.py +++ b/src/animaltrack/web/templates/actions.py @@ -20,6 +20,104 @@ from animaltrack.models.animals import Animal from animaltrack.models.reference import Location, Species from animaltrack.selection.validation import SelectionDiff +# ============================================================================= +# Selection Diff Confirmation Panel +# ============================================================================= + + +def diff_confirmation_panel( + diff: SelectionDiff, + filter_str: str, + resolved_ids: list[str], + roster_hash: str, + ts_utc: int, + action: Callable[..., Any] | str, + action_hidden_fields: list[tuple[str, str]], + cancel_url: str, + confirm_button_text: str, + question_text: str, + confirm_button_cls: str = ButtonT.primary, +) -> Div: + """Create a confirmation panel for selection mismatch scenarios. + + This is a reusable component for all action forms that use optimistic locking. + When the client's selection differs from the server's current state, this panel + shows what changed and asks for confirmation before proceeding. + + Args: + diff: SelectionDiff with added/removed counts. + filter_str: Original filter string. + resolved_ids: Server's resolved IDs (current). + roster_hash: Server's roster hash (current). + ts_utc: Timestamp for resolution. + action: Route function or URL for confirmation submit. + action_hidden_fields: List of (name, value) tuples for action-specific fields. + cancel_url: URL for the cancel button. + confirm_button_text: Text for the confirm button. + question_text: Question shown in the alert (e.g. "Would you like to..."). + confirm_button_cls: Button style class (default: ButtonT.primary). + + Returns: + Div containing the diff panel with confirm button. + """ + # Build description of changes + changes = [] + if diff.removed: + changes.append(f"{len(diff.removed)} animals were removed since you loaded this page") + if diff.added: + changes.append(f"{len(diff.added)} animals were added") + + changes_text = ". ".join(changes) + "." if changes else "The selection has changed." + + # Build confirmation form with hidden fields + resolved_id_fields = [ + Hidden(name="resolved_ids", value=animal_id) for animal_id in resolved_ids + ] + + # Build action-specific hidden fields + action_fields = [Hidden(name=name, value=value) for name, value in action_hidden_fields] + + confirm_form = Form( + *resolved_id_fields, + Hidden(name="filter", value=filter_str), + Hidden(name="roster_hash", value=roster_hash), + *action_fields, + Hidden(name="ts_utc", value=str(ts_utc)), + Hidden(name="confirmed", value="true"), + Hidden(name="nonce", value=str(ULID())), + Div( + Button( + "Cancel", + type="button", + cls=ButtonT.default, + onclick=f"window.location.href='{cancel_url}'", + ), + Button( + confirm_button_text, + type="submit", + cls=confirm_button_cls, + hx_disabled_elt="this", + ), + cls="flex gap-3 mt-4", + ), + action=action, + method="post", + ) + + return Div( + Alert( + Div( + P("Selection Changed", cls="font-bold text-lg mb-2"), + P(changes_text, cls="mb-2"), + P(question_text, cls="text-sm"), + ), + cls=AlertT.warning, + ), + confirm_form, + cls="space-y-4", + ) + + # ============================================================================= # Event Datetime Picker Component # ============================================================================= @@ -680,61 +778,17 @@ def tag_add_diff_panel( Returns: Div containing the diff panel with confirm button. """ - # Build description of changes - changes = [] - if diff.removed: - changes.append(f"{len(diff.removed)} animals were removed since you loaded this page") - if diff.added: - changes.append(f"{len(diff.added)} animals were added") - - changes_text = ". ".join(changes) + "." if changes else "The selection has changed." - - # Build confirmation form with hidden fields - resolved_id_fields = [ - Hidden(name="resolved_ids", value=animal_id) for animal_id in resolved_ids - ] - - confirm_form = Form( - *resolved_id_fields, - Hidden(name="filter", value=filter_str), - Hidden(name="roster_hash", value=roster_hash), - Hidden(name="tag", value=tag), - Hidden(name="ts_utc", value=str(ts_utc)), - Hidden(name="confirmed", value="true"), - Hidden(name="nonce", value=str(ULID())), - Div( - Button( - "Cancel", - type="button", - cls=ButtonT.default, - onclick="window.location.href='/actions/tag-add'", - ), - Button( - f"Confirm Tag ({diff.server_count} animals)", - type="submit", - cls=ButtonT.primary, - hx_disabled_elt="this", - ), - cls="flex gap-3 mt-4", - ), + return diff_confirmation_panel( + diff=diff, + filter_str=filter_str, + resolved_ids=resolved_ids, + roster_hash=roster_hash, + ts_utc=ts_utc, action=action, - method="post", - ) - - return Div( - Alert( - Div( - P("Selection Changed", cls="font-bold text-lg mb-2"), - P(changes_text, cls="mb-2"), - P( - f"Would you like to proceed with tagging {diff.server_count} animals as '{tag}'?", - cls="text-sm", - ), - ), - cls=AlertT.warning, - ), - confirm_form, - cls="space-y-4", + action_hidden_fields=[("tag", tag)], + cancel_url="/actions/tag-add", + confirm_button_text=f"Confirm Tag ({diff.server_count} animals)", + question_text=f"Would you like to proceed with tagging {diff.server_count} animals as '{tag}'?", ) @@ -906,61 +960,17 @@ def tag_end_diff_panel( Returns: Div containing the diff panel with confirm button. """ - # Build description of changes - changes = [] - if diff.removed: - changes.append(f"{len(diff.removed)} animals were removed since you loaded this page") - if diff.added: - changes.append(f"{len(diff.added)} animals were added") - - changes_text = ". ".join(changes) + "." if changes else "The selection has changed." - - # Build confirmation form with hidden fields - resolved_id_fields = [ - Hidden(name="resolved_ids", value=animal_id) for animal_id in resolved_ids - ] - - confirm_form = Form( - *resolved_id_fields, - Hidden(name="filter", value=filter_str), - Hidden(name="roster_hash", value=roster_hash), - Hidden(name="tag", value=tag), - Hidden(name="ts_utc", value=str(ts_utc)), - Hidden(name="confirmed", value="true"), - Hidden(name="nonce", value=str(ULID())), - Div( - Button( - "Cancel", - type="button", - cls=ButtonT.default, - onclick="window.location.href='/actions/tag-end'", - ), - Button( - f"Confirm End Tag ({diff.server_count} animals)", - type="submit", - cls=ButtonT.primary, - hx_disabled_elt="this", - ), - cls="flex gap-3 mt-4", - ), + return diff_confirmation_panel( + diff=diff, + filter_str=filter_str, + resolved_ids=resolved_ids, + roster_hash=roster_hash, + ts_utc=ts_utc, action=action, - method="post", - ) - - return Div( - Alert( - Div( - P("Selection Changed", cls="font-bold text-lg mb-2"), - P(changes_text, cls="mb-2"), - P( - f"Would you like to proceed with ending tag '{tag}' on {diff.server_count} animals?", - cls="text-sm", - ), - ), - cls=AlertT.warning, - ), - confirm_form, - cls="space-y-4", + action_hidden_fields=[("tag", tag)], + cancel_url="/actions/tag-end", + confirm_button_text=f"Confirm End Tag ({diff.server_count} animals)", + question_text=f"Would you like to proceed with ending tag '{tag}' on {diff.server_count} animals?", ) @@ -1154,63 +1164,21 @@ def attrs_diff_panel( Returns: Div containing the diff panel with confirm button. """ - # Build description of changes - changes = [] - if diff.removed: - changes.append(f"{len(diff.removed)} animals were removed since you loaded this page") - if diff.added: - changes.append(f"{len(diff.added)} animals were added") - - changes_text = ". ".join(changes) + "." if changes else "The selection has changed." - - # Build confirmation form with hidden fields - resolved_id_fields = [ - Hidden(name="resolved_ids", value=animal_id) for animal_id in resolved_ids - ] - - confirm_form = Form( - *resolved_id_fields, - Hidden(name="filter", value=filter_str), - Hidden(name="roster_hash", value=roster_hash), - Hidden(name="sex", value=sex or ""), - Hidden(name="life_stage", value=life_stage or ""), - Hidden(name="repro_status", value=repro_status or ""), - Hidden(name="ts_utc", value=str(ts_utc)), - Hidden(name="confirmed", value="true"), - Hidden(name="nonce", value=str(ULID())), - Div( - Button( - "Cancel", - type="button", - cls=ButtonT.default, - onclick="window.location.href='/actions/attrs'", - ), - Button( - f"Confirm Update ({diff.server_count} animals)", - type="submit", - cls=ButtonT.primary, - hx_disabled_elt="this", - ), - cls="flex gap-3 mt-4", - ), + return diff_confirmation_panel( + diff=diff, + filter_str=filter_str, + resolved_ids=resolved_ids, + roster_hash=roster_hash, + ts_utc=ts_utc, action=action, - method="post", - ) - - return Div( - Alert( - Div( - P("Selection Changed", cls="font-bold text-lg mb-2"), - P(changes_text, cls="mb-2"), - P( - f"Would you like to proceed with updating {diff.server_count} animals?", - cls="text-sm", - ), - ), - cls=AlertT.warning, - ), - confirm_form, - cls="space-y-4", + action_hidden_fields=[ + ("sex", sex or ""), + ("life_stage", life_stage or ""), + ("repro_status", repro_status or ""), + ], + cancel_url="/actions/attrs", + confirm_button_text=f"Confirm Update ({diff.server_count} animals)", + question_text=f"Would you like to proceed with updating {diff.server_count} animals?", ) @@ -1456,66 +1424,25 @@ def outcome_diff_panel( Returns: Div containing the diff panel with confirm button. """ - # Build description of changes - changes = [] - if diff.removed: - changes.append(f"{len(diff.removed)} animals were removed since you loaded this page") - if diff.added: - changes.append(f"{len(diff.added)} animals were added") - - changes_text = ". ".join(changes) + "." if changes else "The selection has changed." - - # Build confirmation form with hidden fields - resolved_id_fields = [ - Hidden(name="resolved_ids", value=animal_id) for animal_id in resolved_ids - ] - - confirm_form = Form( - *resolved_id_fields, - Hidden(name="filter", value=filter_str), - Hidden(name="roster_hash", value=roster_hash), - Hidden(name="outcome", value=outcome), - Hidden(name="reason", value=reason or ""), - Hidden(name="yield_product_code", value=yield_product_code or ""), - Hidden(name="yield_unit", value=yield_unit or ""), - Hidden(name="yield_quantity", value=str(yield_quantity) if yield_quantity else ""), - Hidden(name="yield_weight_kg", value=str(yield_weight_kg) if yield_weight_kg else ""), - Hidden(name="ts_utc", value=str(ts_utc)), - Hidden(name="confirmed", value="true"), - Hidden(name="nonce", value=str(ULID())), - Div( - Button( - "Cancel", - type="button", - cls=ButtonT.default, - onclick="window.location.href='/actions/outcome'", - ), - Button( - f"Confirm Outcome ({diff.server_count} animals)", - type="submit", - cls=ButtonT.destructive, - hx_disabled_elt="this", - ), - cls="flex gap-3 mt-4", - ), + return diff_confirmation_panel( + diff=diff, + filter_str=filter_str, + resolved_ids=resolved_ids, + roster_hash=roster_hash, + ts_utc=ts_utc, action=action, - method="post", - ) - - return Div( - Alert( - Div( - P("Selection Changed", cls="font-bold text-lg mb-2"), - P(changes_text, cls="mb-2"), - P( - f"Would you like to proceed with recording {outcome} for {diff.server_count} animals?", - cls="text-sm", - ), - ), - cls=AlertT.warning, - ), - confirm_form, - cls="space-y-4", + action_hidden_fields=[ + ("outcome", outcome), + ("reason", reason or ""), + ("yield_product_code", yield_product_code or ""), + ("yield_unit", yield_unit or ""), + ("yield_quantity", str(yield_quantity) if yield_quantity else ""), + ("yield_weight_kg", str(yield_weight_kg) if yield_weight_kg else ""), + ], + cancel_url="/actions/outcome", + confirm_button_text=f"Confirm Outcome ({diff.server_count} animals)", + question_text=f"Would you like to proceed with recording {outcome} for {diff.server_count} animals?", + confirm_button_cls=ButtonT.destructive, ) @@ -1670,60 +1597,19 @@ def status_correct_diff_panel( Returns: Div containing the diff panel with confirm button. """ - # Build description of changes - changes = [] - if diff.removed: - changes.append(f"{len(diff.removed)} animals were removed since you loaded this page") - if diff.added: - changes.append(f"{len(diff.added)} animals were added") - - changes_text = ". ".join(changes) + "." if changes else "The selection has changed." - - # Build confirmation form with hidden fields - resolved_id_fields = [ - Hidden(name="resolved_ids", value=animal_id) for animal_id in resolved_ids - ] - - confirm_form = Form( - *resolved_id_fields, - Hidden(name="filter", value=filter_str), - Hidden(name="roster_hash", value=roster_hash), - Hidden(name="new_status", value=new_status), - Hidden(name="reason", value=reason), - Hidden(name="ts_utc", value=str(ts_utc)), - Hidden(name="confirmed", value="true"), - Hidden(name="nonce", value=str(ULID())), - Div( - Button( - "Cancel", - type="button", - cls=ButtonT.default, - onclick="window.location.href='/actions/status-correct'", - ), - Button( - f"Confirm Correction ({diff.server_count} animals)", - type="submit", - cls=ButtonT.destructive, - hx_disabled_elt="this", - ), - cls="flex gap-3 mt-4", - ), + return diff_confirmation_panel( + diff=diff, + filter_str=filter_str, + resolved_ids=resolved_ids, + roster_hash=roster_hash, + ts_utc=ts_utc, action=action, - method="post", - ) - - return Div( - Alert( - Div( - P("Selection Changed", cls="font-bold text-lg mb-2"), - P(changes_text, cls="mb-2"), - P( - f"Would you like to proceed with correcting status to {new_status} for {diff.server_count} animals?", - cls="text-sm", - ), - ), - cls=AlertT.warning, - ), - confirm_form, - cls="space-y-4", + action_hidden_fields=[ + ("new_status", new_status), + ("reason", reason), + ], + cancel_url="/actions/status-correct", + confirm_button_text=f"Confirm Correction ({diff.server_count} animals)", + question_text=f"Would you like to proceed with correcting status to {new_status} for {diff.server_count} animals?", + confirm_button_cls=ButtonT.destructive, )