Spaces:
Running
Running
Fix card-number field sizing and non-digit Go! errors
Browse files- src/grades.py +10 -2
- src/streamlit_app.py +35 -8
- src/styles.py +6 -5
- tests/test_grades.py +7 -0
src/grades.py
CHANGED
|
@@ -6,6 +6,7 @@ When the same key appears twice, the row with the later ``modified_at`` wins.
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
| 9 |
from dataclasses import dataclass
|
| 10 |
|
| 11 |
import pandas as pd
|
|
@@ -329,10 +330,17 @@ def plan_progress_sync(
|
|
| 329 |
|
| 330 |
def parse_card_number(raw: object, total_cards: int) -> tuple[int | None, str | None]:
|
| 331 |
"""Parse a 1-based card number, or return ``(None, error)``."""
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
if not text:
|
| 334 |
return None, "Enter a card number."
|
| 335 |
-
if not
|
| 336 |
return None, "Card number must be digits only."
|
| 337 |
number = int(text)
|
| 338 |
if number <= 0:
|
|
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import re
|
| 10 |
from dataclasses import dataclass
|
| 11 |
|
| 12 |
import pandas as pd
|
|
|
|
| 330 |
|
| 331 |
def parse_card_number(raw: object, total_cards: int) -> tuple[int | None, str | None]:
|
| 332 |
"""Parse a 1-based card number, or return ``(None, error)``."""
|
| 333 |
+
if isinstance(raw, bool) or isinstance(raw, float):
|
| 334 |
+
return None, "Card number must be digits only."
|
| 335 |
+
if isinstance(raw, int):
|
| 336 |
+
text = str(raw)
|
| 337 |
+
elif raw is None:
|
| 338 |
+
text = ""
|
| 339 |
+
else:
|
| 340 |
+
text = str(raw).strip()
|
| 341 |
if not text:
|
| 342 |
return None, "Enter a card number."
|
| 343 |
+
if not re.fullmatch(r"[0-9]+", text):
|
| 344 |
return None, "Card number must be digits only."
|
| 345 |
number = int(text)
|
| 346 |
if number <= 0:
|
src/streamlit_app.py
CHANGED
|
@@ -871,8 +871,26 @@ def _wire_card_jump_field() -> None:
|
|
| 871 |
const win = window.parent;
|
| 872 |
const doc = win.document;
|
| 873 |
const size = (inp) => {
|
| 874 |
-
|
| 875 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
};
|
| 877 |
const wire = () => {
|
| 878 |
doc.querySelectorAll(
|
|
@@ -887,16 +905,22 @@ def _wire_card_jump_field() -> None:
|
|
| 887 |
inp.addEventListener('keydown', (e) => {
|
| 888 |
if (e.key !== 'Enter' || e.isComposing) return;
|
| 889 |
e.preventDefault();
|
|
|
|
| 890 |
const btn = doc.querySelector('.st-key-btn_card_go button');
|
| 891 |
if (btn) btn.click();
|
| 892 |
});
|
| 893 |
}
|
| 894 |
}
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 900 |
};
|
| 901 |
wire();
|
| 902 |
if (win.__gradingCardJumpObserver) {
|
|
@@ -1125,8 +1149,10 @@ def _render_card_jump(card_index: int, total_cards: int) -> None:
|
|
| 1125 |
st.session_state.card_jump_to = str(card_index + 1)
|
| 1126 |
err = st.session_state.get("card_jump_error")
|
| 1127 |
if err:
|
|
|
|
| 1128 |
st.markdown(
|
| 1129 |
-
f"<div class='card-jump-error'
|
|
|
|
| 1130 |
unsafe_allow_html=True,
|
| 1131 |
)
|
| 1132 |
form_kwargs: dict[str, object] = {"border": False}
|
|
@@ -1145,6 +1171,7 @@ def _render_card_jump(card_index: int, total_cards: int) -> None:
|
|
| 1145 |
"Card number",
|
| 1146 |
key="card_jump_to",
|
| 1147 |
label_visibility="collapsed",
|
|
|
|
| 1148 |
)
|
| 1149 |
with suffix:
|
| 1150 |
st.markdown(
|
|
|
|
| 871 |
const win = window.parent;
|
| 872 |
const doc = win.document;
|
| 873 |
const size = (inp) => {
|
| 874 |
+
inp.setAttribute('maxlength', '6');
|
| 875 |
+
let v = inp.value || '';
|
| 876 |
+
if (v.length > 6) {
|
| 877 |
+
v = v.slice(0, 6);
|
| 878 |
+
inp.value = v;
|
| 879 |
+
}
|
| 880 |
+
const n = Math.min(6, Math.max(3, v.length));
|
| 881 |
+
inp.style.setProperty('width', n + 'ch', 'important');
|
| 882 |
+
};
|
| 883 |
+
const commit = (inp) => {
|
| 884 |
+
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
| 885 |
+
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
| 886 |
+
};
|
| 887 |
+
const hideIfExpired = () => {
|
| 888 |
+
doc.querySelectorAll('.card-jump-error').forEach((el) => {
|
| 889 |
+
const started = parseFloat(el.getAttribute('data-error-at') || '');
|
| 890 |
+
const age = Number.isFinite(started)
|
| 891 |
+
? Date.now() - started * 1000 : 0;
|
| 892 |
+
el.style.display = age >= 5000 ? 'none' : '';
|
| 893 |
+
});
|
| 894 |
};
|
| 895 |
const wire = () => {
|
| 896 |
doc.querySelectorAll(
|
|
|
|
| 905 |
inp.addEventListener('keydown', (e) => {
|
| 906 |
if (e.key !== 'Enter' || e.isComposing) return;
|
| 907 |
e.preventDefault();
|
| 908 |
+
commit(inp);
|
| 909 |
const btn = doc.querySelector('.st-key-btn_card_go button');
|
| 910 |
if (btn) btn.click();
|
| 911 |
});
|
| 912 |
}
|
| 913 |
}
|
| 914 |
+
hideIfExpired();
|
| 915 |
+
const err = doc.querySelector('.card-jump-error');
|
| 916 |
+
const at = err ? (err.getAttribute('data-error-at') || '') : '';
|
| 917 |
+
if (err && err.style.display !== 'none' && err.dataset.gradingHideTimer !== at) {
|
| 918 |
+
err.dataset.gradingHideTimer = at;
|
| 919 |
+
const started = parseFloat(at);
|
| 920 |
+
const remaining = Number.isFinite(started)
|
| 921 |
+
? Math.max(0, 5000 - (Date.now() - started * 1000)) : 5000;
|
| 922 |
+
setTimeout(hideIfExpired, remaining);
|
| 923 |
+
}
|
| 924 |
};
|
| 925 |
wire();
|
| 926 |
if (win.__gradingCardJumpObserver) {
|
|
|
|
| 1149 |
st.session_state.card_jump_to = str(card_index + 1)
|
| 1150 |
err = st.session_state.get("card_jump_error")
|
| 1151 |
if err:
|
| 1152 |
+
err_at = st.session_state.get("card_jump_error_at") or time.time()
|
| 1153 |
st.markdown(
|
| 1154 |
+
f"<div class='card-jump-error' data-error-at='{float(err_at):.6f}'>"
|
| 1155 |
+
f"{html.escape(str(err))}</div>",
|
| 1156 |
unsafe_allow_html=True,
|
| 1157 |
)
|
| 1158 |
form_kwargs: dict[str, object] = {"border": False}
|
|
|
|
| 1171 |
"Card number",
|
| 1172 |
key="card_jump_to",
|
| 1173 |
label_visibility="collapsed",
|
| 1174 |
+
max_chars=6,
|
| 1175 |
)
|
| 1176 |
with suffix:
|
| 1177 |
st.markdown(
|
src/styles.py
CHANGED
|
@@ -325,12 +325,13 @@ hr, [data-testid="stDivider"] {
|
|
| 325 |
font-variant-numeric: tabular-nums !important;
|
| 326 |
color: var(--text) !important;
|
| 327 |
text-align: center !important;
|
| 328 |
-
line-height: 1.
|
| 329 |
min-height: 0 !important;
|
| 330 |
-
height: 1.
|
| 331 |
-
min-width:
|
| 332 |
-
width:
|
| 333 |
-
|
|
|
|
| 334 |
margin: 0 !important;
|
| 335 |
background: var(--surface-2) !important;
|
| 336 |
border: 1px solid var(--border) !important;
|
|
|
|
| 325 |
font-variant-numeric: tabular-nums !important;
|
| 326 |
color: var(--text) !important;
|
| 327 |
text-align: center !important;
|
| 328 |
+
line-height: 1.875em !important;
|
| 329 |
min-height: 0 !important;
|
| 330 |
+
height: 1.875em !important;
|
| 331 |
+
min-width: 3ch !important;
|
| 332 |
+
max-width: 6ch !important;
|
| 333 |
+
width: 3ch !important;
|
| 334 |
+
padding: 0.075em 0.18em !important;
|
| 335 |
margin: 0 !important;
|
| 336 |
background: var(--surface-2) !important;
|
| 337 |
border: 1px solid var(--border) !important;
|
tests/test_grades.py
CHANGED
|
@@ -362,6 +362,13 @@ def test_parse_card_number_accepts_digits_and_rejects_errors() -> None:
|
|
| 362 |
assert number is None and err is not None
|
| 363 |
number, err = parse_card_number("12a", 10)
|
| 364 |
assert number is None and err is not None and "digits" in err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
number, err = parse_card_number("0", 10)
|
| 366 |
assert number is None and err is not None and "at least 1" in err
|
| 367 |
number, err = parse_card_number("11", 10)
|
|
|
|
| 362 |
assert number is None and err is not None
|
| 363 |
number, err = parse_card_number("12a", 10)
|
| 364 |
assert number is None and err is not None and "digits" in err
|
| 365 |
+
for raw in (".", "1.2", "1e2", "+3", "abc", "3-1", "12.0"):
|
| 366 |
+
number, err = parse_card_number(raw, 10)
|
| 367 |
+
assert number is None and err is not None and "digits" in err, raw
|
| 368 |
+
number, err = parse_card_number(-4, 10)
|
| 369 |
+
assert number is None and err is not None and "digits" in err
|
| 370 |
+
number, err = parse_card_number(1.5, 10)
|
| 371 |
+
assert number is None and err is not None and "digits" in err
|
| 372 |
number, err = parse_card_number("0", 10)
|
| 373 |
assert number is None and err is not None and "at least 1" in err
|
| 374 |
number, err = parse_card_number("11", 10)
|