Spaces:
Running
Running
Reviewer sees review history across all versions
Browse files- Admin still shows one row per trial (latest version), but now displays the
full review history spanning every version, each review tagged with the
version it was made on.
- add list_pair_reviews(); list_submissions() includes all_reviews per trial.
- Current status still reflects the latest version's most recent review; new
reviews attach to the latest version.
- README.md +1 -1
- lib/storage.py +31 -2
- pages/1_Admin.py +13 -9
README.md
CHANGED
|
@@ -24,7 +24,7 @@ A Streamlit intake form for trial statisticians. Submissions are saved to a **Hu
|
|
| 24 |
- `derivation_required` → 4 rubrics: `output.json` × {Inputs used, Calculated value, Method} + `output.R` × {Reproducibility}
|
| 25 |
- Each rubric collects `points`, `tolerance`, `criterion`.
|
| 26 |
- **Versions** — every Submit saves a new version. Re-enter the same `trial_id` + `username`, click **Find versions**, pick one, and **Load selected version** to pull it back into the form for editing; Submit then saves a new version.
|
| 27 |
-
- **Admin page (`pages/1_Admin.py`)** — password-gated review console. Shows **only the latest version of each trial** (one row per `trial_id` + `username`)
|
| 28 |
|
| 29 |
## Run locally
|
| 30 |
|
|
|
|
| 24 |
- `derivation_required` → 4 rubrics: `output.json` × {Inputs used, Calculated value, Method} + `output.R` × {Reproducibility}
|
| 25 |
- Each rubric collects `points`, `tolerance`, `criterion`.
|
| 26 |
- **Versions** — every Submit saves a new version. Re-enter the same `trial_id` + `username`, click **Find versions**, pick one, and **Load selected version** to pull it back into the form for editing; Submit then saves a new version.
|
| 27 |
+
- **Admin page (`pages/1_Admin.py`)** — password-gated review console. Shows **only the latest version of each trial** (one row per `trial_id` + `username`), but the review history covers **all versions** of that trial (each review tagged with the version it was made on). The current status reflects the latest version's most recent review. New reviews are added to the latest version. Each review is its own file under `reviews/<trial>__<user>/<version>/`. (Submitters can still see and load all their own versions on the form.)
|
| 28 |
|
| 29 |
## Run locally
|
| 30 |
|
lib/storage.py
CHANGED
|
@@ -242,8 +242,33 @@ def add_review(submission_id: str, status: str, reviewer: str, note: str = "") -
|
|
| 242 |
return review
|
| 243 |
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
def list_reviews(submission_id: str, all_files: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
| 246 |
-
"""All reviews for a submission, oldest first."""
|
| 247 |
base = _base_id(submission_id)
|
| 248 |
prefix = f"{REVIEWS_PREFIX}/{base}/"
|
| 249 |
files = all_files if all_files is not None else _all_files()
|
|
@@ -290,12 +315,15 @@ def list_submissions() -> List[Dict[str, Any]]:
|
|
| 290 |
latest_by_pair[key] = sp
|
| 291 |
|
| 292 |
result: List[Dict[str, Any]] = []
|
| 293 |
-
for sp in latest_by_pair.
|
| 294 |
sub = _read_json(sp)
|
| 295 |
if not sub:
|
| 296 |
continue
|
|
|
|
| 297 |
reviews = list_reviews(sp, all_files=files)
|
| 298 |
latest = reviews[-1] if reviews else None
|
|
|
|
|
|
|
| 299 |
result.append(
|
| 300 |
{
|
| 301 |
"submissionId": sp,
|
|
@@ -308,6 +336,7 @@ def list_submissions() -> List[Dict[str, Any]]:
|
|
| 308 |
"reviewer": latest["reviewer"] if latest else "",
|
| 309 |
"review_count": len(reviews),
|
| 310 |
"reviews": reviews,
|
|
|
|
| 311 |
"submission": sub,
|
| 312 |
}
|
| 313 |
)
|
|
|
|
| 242 |
return review
|
| 243 |
|
| 244 |
|
| 245 |
+
def list_pair_reviews(
|
| 246 |
+
pair_key: str, all_files: Optional[List[str]] = None
|
| 247 |
+
) -> List[Dict[str, Any]]:
|
| 248 |
+
"""Every review across ALL versions of a (trial_id, username) pair.
|
| 249 |
+
|
| 250 |
+
pair_key is '<trial>__<user>'. Each returned review is tagged with the
|
| 251 |
+
`version` it was made on. Oldest first.
|
| 252 |
+
"""
|
| 253 |
+
prefix = f"{REVIEWS_PREFIX}/{pair_key}/"
|
| 254 |
+
files = all_files if all_files is not None else _all_files()
|
| 255 |
+
paths = sorted(f for f in files if f.startswith(prefix) and f.endswith(".json"))
|
| 256 |
+
out: List[Dict[str, Any]] = []
|
| 257 |
+
for p in paths:
|
| 258 |
+
rec = _read_json(p)
|
| 259 |
+
if not rec:
|
| 260 |
+
continue
|
| 261 |
+
# p = reviews/<pair>/<version>/<revfile>.json
|
| 262 |
+
parts = p[len(REVIEWS_PREFIX) + 1 :].split("/")
|
| 263 |
+
rec = dict(rec)
|
| 264 |
+
rec["version"] = parts[1] if len(parts) >= 3 else ""
|
| 265 |
+
out.append(rec)
|
| 266 |
+
out.sort(key=lambda r: r.get("at", ""))
|
| 267 |
+
return out
|
| 268 |
+
|
| 269 |
+
|
| 270 |
def list_reviews(submission_id: str, all_files: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
| 271 |
+
"""All reviews for a single submission version, oldest first."""
|
| 272 |
base = _base_id(submission_id)
|
| 273 |
prefix = f"{REVIEWS_PREFIX}/{base}/"
|
| 274 |
files = all_files if all_files is not None else _all_files()
|
|
|
|
| 315 |
latest_by_pair[key] = sp
|
| 316 |
|
| 317 |
result: List[Dict[str, Any]] = []
|
| 318 |
+
for key, sp in latest_by_pair.items():
|
| 319 |
sub = _read_json(sp)
|
| 320 |
if not sub:
|
| 321 |
continue
|
| 322 |
+
# Reviews on the latest version drive the current status.
|
| 323 |
reviews = list_reviews(sp, all_files=files)
|
| 324 |
latest = reviews[-1] if reviews else None
|
| 325 |
+
# All reviews across every version of this trial (tagged with version).
|
| 326 |
+
all_reviews = list_pair_reviews(key, all_files=files)
|
| 327 |
result.append(
|
| 328 |
{
|
| 329 |
"submissionId": sp,
|
|
|
|
| 336 |
"reviewer": latest["reviewer"] if latest else "",
|
| 337 |
"review_count": len(reviews),
|
| 338 |
"reviews": reviews,
|
| 339 |
+
"all_reviews": all_reviews,
|
| 340 |
"submission": sub,
|
| 341 |
}
|
| 342 |
)
|
pages/1_Admin.py
CHANGED
|
@@ -107,26 +107,30 @@ for s in items:
|
|
| 107 |
)
|
| 108 |
with meta_c2:
|
| 109 |
st.markdown(
|
| 110 |
-
f"**
|
| 111 |
f"**Last reviewer:** {s.get('reviewer', '') or '—'}"
|
| 112 |
)
|
| 113 |
|
| 114 |
-
# ---- Review
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
| 118 |
st.caption("No reviews yet.")
|
| 119 |
else:
|
| 120 |
-
for rev in reversed(
|
|
|
|
|
|
|
|
|
|
| 121 |
st.markdown(
|
| 122 |
f"- {status_badge(rev.get('status', ''))} — "
|
| 123 |
f"**{rev.get('reviewer') or 'anon'}** "
|
| 124 |
-
f"· _{rev.get('at', '')}_"
|
| 125 |
+ (f" \n {rev.get('note')}" if rev.get("note") else "")
|
| 126 |
)
|
| 127 |
|
| 128 |
-
# ---- Add a review -----------
|
| 129 |
-
st.markdown("#### Add a review")
|
| 130 |
with st.form(f"review_{s['submissionId']}"):
|
| 131 |
new_status = st.radio(
|
| 132 |
"Status",
|
|
|
|
| 107 |
)
|
| 108 |
with meta_c2:
|
| 109 |
st.markdown(
|
| 110 |
+
f"**Latest version:** `{s.get('version', '')}` \n"
|
| 111 |
f"**Last reviewer:** {s.get('reviewer', '') or '—'}"
|
| 112 |
)
|
| 113 |
|
| 114 |
+
# ---- Review history across ALL versions of this trial --------
|
| 115 |
+
all_reviews = s.get("all_reviews") or []
|
| 116 |
+
current_version = s.get("version", "")
|
| 117 |
+
st.markdown(f"#### Review history — all versions ({len(all_reviews)})")
|
| 118 |
+
if not all_reviews:
|
| 119 |
st.caption("No reviews yet.")
|
| 120 |
else:
|
| 121 |
+
for rev in reversed(all_reviews): # newest first
|
| 122 |
+
rev_version = rev.get("version", "")
|
| 123 |
+
is_current = rev_version == current_version
|
| 124 |
+
vtag = f"`v{rev_version}`" + (" _(current)_" if is_current else "")
|
| 125 |
st.markdown(
|
| 126 |
f"- {status_badge(rev.get('status', ''))} — "
|
| 127 |
f"**{rev.get('reviewer') or 'anon'}** "
|
| 128 |
+
f"· _{rev.get('at', '')}_ · on {vtag}"
|
| 129 |
+ (f" \n {rev.get('note')}" if rev.get("note") else "")
|
| 130 |
)
|
| 131 |
|
| 132 |
+
# ---- Add a review (applies to the latest version) -----------
|
| 133 |
+
st.markdown(f"#### Add a review — on latest version `v{s.get('version', '')}`")
|
| 134 |
with st.form(f"review_{s['submissionId']}"):
|
| 135 |
new_status = st.radio(
|
| 136 |
"Status",
|