tttjjj commited on
Commit
f3a87c7
·
1 Parent(s): fa43f54

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.

Files changed (3) hide show
  1. README.md +1 -1
  2. lib/storage.py +31 -2
  3. 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`). A version can be reviewed many times by different people: each review (status + reviewer name + comment) is written as its own file under `reviews/<trial>__<user>/<version>/`, and the page shows the full timeline. The current status is the most recent review's status. (Submitters can still see and load all their own versions on the form.)
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.values():
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"**File:** `{s.get('submissionId', '')}` \n"
111
  f"**Last reviewer:** {s.get('reviewer', '') or '—'}"
112
  )
113
 
114
- # ---- Review timeline -----------------------------------------
115
- reviews = s.get("reviews") or []
116
- st.markdown(f"#### Review history ({len(reviews)})")
117
- if not reviews:
 
118
  st.caption("No reviews yet.")
119
  else:
120
- for rev in reversed(reviews): # newest first
 
 
 
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",