Spaces:
Sleeping
Sleeping
Refine documentation and fix upload handling
Browse files- Update MANUAL_TESTING.md to clarify parsing behavior for multi-column files.
- Correct title and formatting in guide.html and index.html for consistency.
- Enhance README.md to specify behavior for skipped duplicates and blank lines.
- Implement unique ID assignment for corpus uploads in main.py to prevent file clobbering.
- Add regression test in test_api.py to ensure distinct uploads do not overwrite each other.
- MANUAL_TESTING.md +2 -1
- backend/app/guide.html +12 -12
- backend/app/main.py +5 -0
- backend/tests/test_api.py +38 -0
- frontend/index.html +1 -1
- sample_data/README.md +1 -1
MANUAL_TESTING.md
CHANGED
|
@@ -63,7 +63,8 @@ with a "Sign in (top right)" message. Sign in and retry: accepted.
|
|
| 63 |
2. Try a CSV with `item,reverse` columns (1/true/yes/R = reverse) or a
|
| 64 |
single-column file with `(R)` markers.
|
| 65 |
3. Expect: items fill the textarea ((R) appended where flagged), the filename
|
| 66 |
-
becomes the suggested name, and parse notes list skipped
|
|
|
|
| 67 |
Nothing is saved until you review and press Save. Item files are never
|
| 68 |
retained on the server.
|
| 69 |
|
|
|
|
| 63 |
2. Try a CSV with `item,reverse` columns (1/true/yes/R = reverse) or a
|
| 64 |
single-column file with `(R)` markers.
|
| 65 |
3. Expect: items fill the textarea ((R) appended where flagged), the filename
|
| 66 |
+
becomes the suggested name, and parse notes list skipped duplicates (and
|
| 67 |
+
blank cells in multi-column files; fully blank lines are dropped at read).
|
| 68 |
Nothing is saved until you review and press Save. Item files are never
|
| 69 |
retained on the server.
|
| 70 |
|
backend/app/guide.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>CCR Platform
|
| 7 |
<style>
|
| 8 |
:root {
|
| 9 |
--bg: #ffffff; --fg: #1a1a1a; --muted: #666; --border: #ddd;
|
|
@@ -49,16 +49,16 @@
|
|
| 49 |
<body>
|
| 50 |
<main>
|
| 51 |
|
| 52 |
-
<h1>CCR Platform
|
| 53 |
<p class="sub">Everything built so far, as click-through scenarios: what to do and exactly what
|
| 54 |
-
you should see. Every dataset referenced is downloadable right here
|
| 55 |
synthetic, written for this kit; no real participant data anywhere.</p>
|
| 56 |
|
| 57 |
<div class="note">
|
| 58 |
-
<b>⚠ This is the free dev instance
|
| 59 |
<ul>
|
| 60 |
<li><b>Storage is ephemeral.</b> Accounts, projects, and saved runs are wiped whenever
|
| 61 |
-
the instance rebuilds or restarts. If your account disappears, that's expected
|
| 62 |
just re-register. (Google sign-in recreates itself automatically.)</li>
|
| 63 |
<li><b>It sleeps when idle.</b> After ~48 h without visitors, the first page load
|
| 64 |
takes about a minute to wake it up.</li>
|
|
@@ -138,12 +138,12 @@ are deleted after analysis. Upload
|
|
| 138 |
<ol>
|
| 139 |
<li>"+ Custom construct" > "Upload items from CSV/XLSX".</li>
|
| 140 |
<li>Try <a href="/samples/construct_items_demo.csv" download>construct_items_demo.csv</a>
|
| 141 |
-
(<code>item,reverse</code> columns
|
| 142 |
<a href="/samples/construct_items_marker_demo.csv" download>construct_items_marker_demo.csv</a>
|
| 143 |
(single column with <code>(R)</code> markers), or
|
| 144 |
<a href="/samples/construct_items_demo.xlsx" download>construct_items_demo.xlsx</a> (Excel).</li>
|
| 145 |
<li>Expect: items fill the textarea ((R) appended where flagged), the filename becomes
|
| 146 |
-
the suggested name, and parse notes list skipped
|
| 147 |
until you review and press Save. Item files are never retained on the server.</li>
|
| 148 |
</ol>
|
| 149 |
|
|
@@ -159,7 +159,7 @@ are deleted after analysis. Upload
|
|
| 159 |
<tr><td>french_demo.csv</td><td>fr + Multilingual E5</td><td>no language warnings</td></tr>
|
| 160 |
<tr><td><a href="/samples/mixed_language_demo.csv" download>mixed_language_demo.csv</a></td><td>en + MiniLM</td><td>LANGUAGE_UNCERTAIN (majority 50%)</td></tr>
|
| 161 |
<tr><td><a href="/samples/long_documents_demo.csv" download>long_documents_demo.csv</a></td><td>en + MiniLM</td>
|
| 162 |
-
<td>TEXTS_MAYBE_TRUNCATED (4) + LANGUAGE_UNCERTAIN (only 10 rows, below the 20-row minimum
|
| 163 |
</table>
|
| 164 |
</div>
|
| 165 |
<p>Warnings are per-run snapshots: changing language/model requires a NEW run; old result
|
|
@@ -181,12 +181,12 @@ pages don't update.</p>
|
|
| 181 |
|
| 182 |
<h2 id="accounts">6. Accounts</h2>
|
| 183 |
<ol>
|
| 184 |
-
<li>Sign in (top right) > "Create a free account" > email + password (min 8 chars)
|
| 185 |
or use "Continue with Google".</li>
|
| 186 |
<li>You're signed in immediately; the header shows your name.</li>
|
| 187 |
<li>Sign out, sign back in; wrong password gives "Incorrect email or password";
|
| 188 |
registering the same email again gives "already exists".</li>
|
| 189 |
-
<li>Email is case-insensitive. No self-service password reset yet
|
| 190 |
(And remember: password accounts vanish when this dev instance rebuilds.)</li>
|
| 191 |
</ol>
|
| 192 |
|
|
@@ -223,8 +223,8 @@ pages don't update.</p>
|
|
| 223 |
</ol>
|
| 224 |
|
| 225 |
<h2 id="feedback">10. Found something off?</h2>
|
| 226 |
-
<p>Anything that doesn't match what this guide says it should do
|
| 227 |
-
slow, or missing
|
| 228 |
Deva (<a href="mailto:devaanand@umass.edu">devaanand@umass.edu</a>). Screenshots welcome.</p>
|
| 229 |
|
| 230 |
</main>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>CCR Platform - Testing Guide</title>
|
| 7 |
<style>
|
| 8 |
:root {
|
| 9 |
--bg: #ffffff; --fg: #1a1a1a; --muted: #666; --border: #ddd;
|
|
|
|
| 49 |
<body>
|
| 50 |
<main>
|
| 51 |
|
| 52 |
+
<h1>CCR Platform - Testing Guide</h1>
|
| 53 |
<p class="sub">Everything built so far, as click-through scenarios: what to do and exactly what
|
| 54 |
+
you should see. Every dataset referenced is downloadable right here - all texts are
|
| 55 |
synthetic, written for this kit; no real participant data anywhere.</p>
|
| 56 |
|
| 57 |
<div class="note">
|
| 58 |
+
<b>⚠ This is the free dev instance - two caveats before you start:</b>
|
| 59 |
<ul>
|
| 60 |
<li><b>Storage is ephemeral.</b> Accounts, projects, and saved runs are wiped whenever
|
| 61 |
+
the instance rebuilds or restarts. If your account disappears, that's expected -
|
| 62 |
just re-register. (Google sign-in recreates itself automatically.)</li>
|
| 63 |
<li><b>It sleeps when idle.</b> After ~48 h without visitors, the first page load
|
| 64 |
takes about a minute to wake it up.</li>
|
|
|
|
| 138 |
<ol>
|
| 139 |
<li>"+ Custom construct" > "Upload items from CSV/XLSX".</li>
|
| 140 |
<li>Try <a href="/samples/construct_items_demo.csv" download>construct_items_demo.csv</a>
|
| 141 |
+
(<code>item,reverse</code> columns - 1/true/yes/R = reverse),
|
| 142 |
<a href="/samples/construct_items_marker_demo.csv" download>construct_items_marker_demo.csv</a>
|
| 143 |
(single column with <code>(R)</code> markers), or
|
| 144 |
<a href="/samples/construct_items_demo.xlsx" download>construct_items_demo.xlsx</a> (Excel).</li>
|
| 145 |
<li>Expect: items fill the textarea ((R) appended where flagged), the filename becomes
|
| 146 |
+
the suggested name, and parse notes list skipped duplicates. Nothing is saved
|
| 147 |
until you review and press Save. Item files are never retained on the server.</li>
|
| 148 |
</ol>
|
| 149 |
|
|
|
|
| 159 |
<tr><td>french_demo.csv</td><td>fr + Multilingual E5</td><td>no language warnings</td></tr>
|
| 160 |
<tr><td><a href="/samples/mixed_language_demo.csv" download>mixed_language_demo.csv</a></td><td>en + MiniLM</td><td>LANGUAGE_UNCERTAIN (majority 50%)</td></tr>
|
| 161 |
<tr><td><a href="/samples/long_documents_demo.csv" download>long_documents_demo.csv</a></td><td>en + MiniLM</td>
|
| 162 |
+
<td>TEXTS_MAYBE_TRUNCATED (4) + LANGUAGE_UNCERTAIN (only 10 rows, below the 20-row minimum - by design)</td></tr>
|
| 163 |
</table>
|
| 164 |
</div>
|
| 165 |
<p>Warnings are per-run snapshots: changing language/model requires a NEW run; old result
|
|
|
|
| 181 |
|
| 182 |
<h2 id="accounts">6. Accounts</h2>
|
| 183 |
<ol>
|
| 184 |
+
<li>Sign in (top right) > "Create a free account" > email + password (min 8 chars) -
|
| 185 |
or use "Continue with Google".</li>
|
| 186 |
<li>You're signed in immediately; the header shows your name.</li>
|
| 187 |
<li>Sign out, sign back in; wrong password gives "Incorrect email or password";
|
| 188 |
registering the same email again gives "already exists".</li>
|
| 189 |
+
<li>Email is case-insensitive. No self-service password reset yet - reset = admin action.
|
| 190 |
(And remember: password accounts vanish when this dev instance rebuilds.)</li>
|
| 191 |
</ol>
|
| 192 |
|
|
|
|
| 223 |
</ol>
|
| 224 |
|
| 225 |
<h2 id="feedback">10. Found something off?</h2>
|
| 226 |
+
<p>Anything that doesn't match what this guide says it should do - or anything confusing,
|
| 227 |
+
slow, or missing - please note the section number and what you saw, and send it to
|
| 228 |
Deva (<a href="mailto:devaanand@umass.edu">devaanand@umass.edu</a>). Screenshots welcome.</p>
|
| 229 |
|
| 230 |
</main>
|
backend/app/main.py
CHANGED
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
| 11 |
|
| 12 |
import json
|
| 13 |
import os
|
|
|
|
| 14 |
from contextlib import asynccontextmanager
|
| 15 |
from pathlib import Path
|
| 16 |
|
|
@@ -483,7 +484,11 @@ async def upload_corpus(
|
|
| 483 |
f"Anonymous uploads are limited to {mb} MB. Sign in (top right) to upload larger files.",
|
| 484 |
)
|
| 485 |
|
|
|
|
|
|
|
|
|
|
| 486 |
corpus = Corpus(
|
|
|
|
| 487 |
project_id=project_id, filename=file.filename, path="", n_rows=0, columns_json="[]"
|
| 488 |
)
|
| 489 |
# Parse from a local temp file, then hand the bytes to the storage backend
|
|
|
|
| 11 |
|
| 12 |
import json
|
| 13 |
import os
|
| 14 |
+
import uuid
|
| 15 |
from contextlib import asynccontextmanager
|
| 16 |
from pathlib import Path
|
| 17 |
|
|
|
|
| 484 |
f"Anonymous uploads are limited to {mb} MB. Sign in (top right) to upload larger files.",
|
| 485 |
)
|
| 486 |
|
| 487 |
+
# Assign the id NOW: the model's default fires at INSERT flush, so reading
|
| 488 |
+
# corpus.id before commit yields None - every upload would then share one
|
| 489 |
+
# "None.csv" on disk, each new upload silently clobbering the previous one.
|
| 490 |
corpus = Corpus(
|
| 491 |
+
id=uuid.uuid4().hex,
|
| 492 |
project_id=project_id, filename=file.filename, path="", n_rows=0, columns_json="[]"
|
| 493 |
)
|
| 494 |
# Parse from a local temp file, then hand the bytes to the storage backend
|
backend/tests/test_api.py
CHANGED
|
@@ -269,3 +269,41 @@ def test_script_export_is_valid_offline_python(client, flow):
|
|
| 269 |
reqs = client.get(f"/api/jobs/{job_id}/script-requirements")
|
| 270 |
assert reqs.status_code == 200
|
| 271 |
assert "==" in reqs.text # pinned versions, not ranges
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
reqs = client.get(f"/api/jobs/{job_id}/script-requirements")
|
| 270 |
assert reqs.status_code == 200
|
| 271 |
assert "==" in reqs.text # pinned versions, not ranges
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
# ------------------------------------------- uploads must not clobber each other
|
| 275 |
+
def test_second_upload_does_not_clobber_first(client, flow):
|
| 276 |
+
"""Regression: corpus ids used to be read before INSERT (still None), so
|
| 277 |
+
every upload stored to the same "None.csv" - a later upload silently
|
| 278 |
+
replaced an earlier corpus's file, and runs analyzed the wrong dataset."""
|
| 279 |
+
project = client.post("/api/projects", json={"name": "clobber-check"}).json()
|
| 280 |
+
|
| 281 |
+
first = upload(client, project["id"], "first.csv",
|
| 282 |
+
b"text\nalpha one\nalpha two\nalpha three\n").json()
|
| 283 |
+
second = upload(client, project["id"], "second.csv",
|
| 284 |
+
b"text\nbeta one\nbeta two\nbeta three\nbeta four\n").json()
|
| 285 |
+
|
| 286 |
+
# Distinct stored files, both still present after the second upload.
|
| 287 |
+
from pathlib import Path
|
| 288 |
+
assert first["id"] and second["id"] and first["id"] != second["id"]
|
| 289 |
+
paths = {
|
| 290 |
+
c["filename"]: c for c in
|
| 291 |
+
client.get(f"/api/projects/{project['id']}/corpora").json()
|
| 292 |
+
}
|
| 293 |
+
assert paths["first.csv"]["n_rows"] == 3 and paths["second.csv"]["n_rows"] == 4
|
| 294 |
+
|
| 295 |
+
# Running the FIRST corpus must analyze the first corpus's rows.
|
| 296 |
+
job = client.post("/api/jobs", json={
|
| 297 |
+
"project_id": project["id"], "corpus_id": first["id"],
|
| 298 |
+
"construct_id": flow["construct"]["id"], "text_column": "text",
|
| 299 |
+
"model_name": "fake-deterministic",
|
| 300 |
+
}).json()
|
| 301 |
+
job = wait_for_job(client, job["id"])
|
| 302 |
+
assert job["status"] == "completed", job.get("error")
|
| 303 |
+
# Assert on the EXPORTED rows (what was actually analyzed), not on DB
|
| 304 |
+
# metadata - the broken path kept correct metadata while scoring the
|
| 305 |
+
# other corpus's texts.
|
| 306 |
+
export = client.get(f"/api/jobs/{job['id']}/export").text
|
| 307 |
+
data_lines = [l for l in export.splitlines()[1:] if l.strip()]
|
| 308 |
+
assert len(data_lines) == 3, export
|
| 309 |
+
assert "alpha" in export and "beta" not in export
|
frontend/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>CCR Platform
|
| 7 |
</head>
|
| 8 |
<body>
|
| 9 |
<div id="root"></div>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>CCR Platform - Contextualized Construct Representations</title>
|
| 7 |
</head>
|
| 8 |
<body>
|
| 9 |
<div id="root"></div>
|
sample_data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ select language English and model MiniLM.
|
|
| 19 |
| `xlsx_upload_demo.xlsx` | Excel ingestion path | Uploads and parses like a CSV |
|
| 20 |
| `large_demo.csv` | Anonymous upload caps (800 rows) | Signed out: blocked with the sign-in hint. Signed in: runs |
|
| 21 |
| `construct_items_demo.csv` | Construct upload: item + reverse columns | + Custom construct > upload: 6 items parse, last one flagged reverse-scored |
|
| 22 |
-
| `construct_items_marker_demo.csv` | Construct upload: (R) markers, blank + duplicate rows | 4 items parse (blank
|
| 23 |
| `construct_items_demo.xlsx` | Construct upload from Excel | 4 items, two flagged reverse via (R) markers |
|
| 24 |
|
| 25 |
Notes:
|
|
|
|
| 19 |
| `xlsx_upload_demo.xlsx` | Excel ingestion path | Uploads and parses like a CSV |
|
| 20 |
| `large_demo.csv` | Anonymous upload caps (800 rows) | Signed out: blocked with the sign-in hint. Signed in: runs |
|
| 21 |
| `construct_items_demo.csv` | Construct upload: item + reverse columns | + Custom construct > upload: 6 items parse, last one flagged reverse-scored |
|
| 22 |
+
| `construct_items_marker_demo.csv` | Construct upload: (R) markers, blank + duplicate rows | 4 items parse; notes report the skipped duplicate (the blank line is dropped silently at read); "(R)" line flagged |
|
| 23 |
| `construct_items_demo.xlsx` | Construct upload from Excel | 4 items, two flagged reverse via (R) markers |
|
| 24 |
|
| 25 |
Notes:
|