Spaces:
Running
Running
Deploy Rachana Data Studio
Browse files- data_studio/app.py +23 -18
- data_studio/auth.py +14 -3
- data_studio/review.py +3 -2
- data_studio/security.py +3 -3
- frontend/app/page.tsx +2 -2
data_studio/app.py
CHANGED
|
@@ -283,24 +283,29 @@ def render_transliteration_editor(sample: dict[str, Any]) -> dict[str, Any]:
|
|
| 283 |
return {"current_transliteration": {"native_text": native_text, "latin_text": latin_text}}
|
| 284 |
|
| 285 |
|
| 286 |
-
def render_review(db, user: dict[str, Any]) -> None:
|
| 287 |
-
st.subheader("Review queue")
|
| 288 |
-
counts = queue_status_counts(db)
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
queue_stats = counts.get(
|
| 294 |
-
stats_cols = st.columns(4)
|
| 295 |
-
stats_cols[0].metric("Pending", queue_stats.get("pending", 0))
|
| 296 |
-
stats_cols[1].metric("Accepted", queue_stats.get("accepted", 0))
|
| 297 |
-
stats_cols[2].metric("Edited", queue_stats.get("edited", 0))
|
| 298 |
-
stats_cols[3].metric("Rejected", queue_stats.get("rejected", 0))
|
| 299 |
-
|
| 300 |
-
sample = claim_next_sample(
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
st.caption(f"Sample ID: {sample['sample_id']}")
|
| 306 |
meta_cols = st.columns(4)
|
|
|
|
| 283 |
return {"current_transliteration": {"native_text": native_text, "latin_text": latin_text}}
|
| 284 |
|
| 285 |
|
| 286 |
+
def render_review(db, user: dict[str, Any]) -> None:
|
| 287 |
+
st.subheader("Review queue")
|
| 288 |
+
counts = queue_status_counts(db)
|
| 289 |
+
sources = [source for source in list_sources(db) if source.get("enabled", True)]
|
| 290 |
+
dataset_options = ["All active datasets"] + [source["dataset_key"] for source in sources]
|
| 291 |
+
selected_dataset = st.selectbox("Dataset", dataset_options)
|
| 292 |
+
queue_name = "all"
|
| 293 |
+
queue_stats = counts.get("pure_telugu", {})
|
| 294 |
+
stats_cols = st.columns(4)
|
| 295 |
+
stats_cols[0].metric("Pending", queue_stats.get("pending", 0))
|
| 296 |
+
stats_cols[1].metric("Accepted", queue_stats.get("accepted", 0))
|
| 297 |
+
stats_cols[2].metric("Edited", queue_stats.get("edited", 0))
|
| 298 |
+
stats_cols[3].metric("Rejected", queue_stats.get("rejected", 0))
|
| 299 |
+
|
| 300 |
+
sample = claim_next_sample(
|
| 301 |
+
db,
|
| 302 |
+
queue_name,
|
| 303 |
+
user["username"],
|
| 304 |
+
None if selected_dataset == "All active datasets" else selected_dataset,
|
| 305 |
+
)
|
| 306 |
+
if sample is None:
|
| 307 |
+
st.info("No pending samples for the selected pool.")
|
| 308 |
+
return
|
| 309 |
|
| 310 |
st.caption(f"Sample ID: {sample['sample_id']}")
|
| 311 |
meta_cols = st.columns(4)
|
data_studio/auth.py
CHANGED
|
@@ -27,6 +27,17 @@ def single_user_username() -> str:
|
|
| 27 |
return normalize_username(os.getenv("SINGLE_USER_USERNAME", ""))
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def is_single_user_owner(username: str) -> bool:
|
| 31 |
return single_user_mode_enabled() and normalize_username(username) == single_user_username()
|
| 32 |
|
|
@@ -63,7 +74,7 @@ def serialize_user(user: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
| 63 |
if user is None:
|
| 64 |
return None
|
| 65 |
normalized_username = normalize_username(user["username"])
|
| 66 |
-
role = "owner" if is_single_user_owner(normalized_username) else user.get("role", "reviewer")
|
| 67 |
return {
|
| 68 |
"username": normalized_username,
|
| 69 |
"display_name": user.get("display_name") or normalized_username,
|
|
@@ -111,11 +122,11 @@ def create_or_update_user(
|
|
| 111 |
payload = {
|
| 112 |
"username": normalized_username,
|
| 113 |
"display_name": (display_name or normalized_username).strip(),
|
| 114 |
-
"role": "admin" if is_single_user_owner(normalized_username) else (role.strip() or "reviewer"),
|
| 115 |
"is_active": bool(is_active),
|
| 116 |
"password": password_record,
|
| 117 |
"updated_at": now,
|
| 118 |
-
}
|
| 119 |
update_doc = {
|
| 120 |
"$set": payload,
|
| 121 |
"$setOnInsert": {"created_at": now, "last_login_at": None},
|
|
|
|
| 27 |
return normalize_username(os.getenv("SINGLE_USER_USERNAME", ""))
|
| 28 |
|
| 29 |
|
| 30 |
+
def admin_usernames() -> set[str]:
|
| 31 |
+
raw = os.getenv("ADMIN_USERNAMES", "prashanth").strip()
|
| 32 |
+
if not raw:
|
| 33 |
+
return set()
|
| 34 |
+
return {normalize_username(item) for item in raw.split(",") if item.strip()}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def is_admin_username(username: str) -> bool:
|
| 38 |
+
return normalize_username(username) in admin_usernames()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
def is_single_user_owner(username: str) -> bool:
|
| 42 |
return single_user_mode_enabled() and normalize_username(username) == single_user_username()
|
| 43 |
|
|
|
|
| 74 |
if user is None:
|
| 75 |
return None
|
| 76 |
normalized_username = normalize_username(user["username"])
|
| 77 |
+
role = "owner" if is_single_user_owner(normalized_username) else "admin" if is_admin_username(normalized_username) else user.get("role", "reviewer")
|
| 78 |
return {
|
| 79 |
"username": normalized_username,
|
| 80 |
"display_name": user.get("display_name") or normalized_username,
|
|
|
|
| 122 |
payload = {
|
| 123 |
"username": normalized_username,
|
| 124 |
"display_name": (display_name or normalized_username).strip(),
|
| 125 |
+
"role": "admin" if (is_single_user_owner(normalized_username) or is_admin_username(normalized_username)) else (role.strip() or "reviewer"),
|
| 126 |
"is_active": bool(is_active),
|
| 127 |
"password": password_record,
|
| 128 |
"updated_at": now,
|
| 129 |
+
}
|
| 130 |
update_doc = {
|
| 131 |
"$set": payload,
|
| 132 |
"$setOnInsert": {"created_at": now, "last_login_at": None},
|
data_studio/review.py
CHANGED
|
@@ -64,8 +64,7 @@ def claim_next_sample(
|
|
| 64 |
) -> dict[str, Any] | None:
|
| 65 |
now = utc_now()
|
| 66 |
expires_at = now + timedelta(minutes=LEASE_MINUTES)
|
| 67 |
-
lease_filter = {
|
| 68 |
-
"queue_name": queue_name,
|
| 69 |
"status": "pending",
|
| 70 |
"$or": [
|
| 71 |
{"lease": {"$exists": False}},
|
|
@@ -73,6 +72,8 @@ def claim_next_sample(
|
|
| 73 |
{"lease.reviewer": reviewer},
|
| 74 |
],
|
| 75 |
}
|
|
|
|
|
|
|
| 76 |
if dataset_key:
|
| 77 |
lease_filter["source.dataset_key"] = dataset_key
|
| 78 |
candidate_ids = [
|
|
|
|
| 64 |
) -> dict[str, Any] | None:
|
| 65 |
now = utc_now()
|
| 66 |
expires_at = now + timedelta(minutes=LEASE_MINUTES)
|
| 67 |
+
lease_filter: dict[str, Any] = {
|
|
|
|
| 68 |
"status": "pending",
|
| 69 |
"$or": [
|
| 70 |
{"lease": {"$exists": False}},
|
|
|
|
| 72 |
{"lease.reviewer": reviewer},
|
| 73 |
],
|
| 74 |
}
|
| 75 |
+
if queue_name not in {"", "all", "*"}:
|
| 76 |
+
lease_filter["queue_name"] = queue_name
|
| 77 |
if dataset_key:
|
| 78 |
lease_filter["source.dataset_key"] = dataset_key
|
| 79 |
candidate_ids = [
|
data_studio/security.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
| 7 |
|
| 8 |
from pymongo.database import Database
|
| 9 |
|
| 10 |
-
from data_studio.auth import authenticate_user, get_active_user, is_single_user_owner, serialize_user
|
| 11 |
from data_studio.utils import utc_now
|
| 12 |
|
| 13 |
|
|
@@ -53,7 +53,7 @@ def permissions_for_role(role: str) -> list[str]:
|
|
| 53 |
|
| 54 |
|
| 55 |
def permissions_for_user(user: dict[str, Any]) -> list[str]:
|
| 56 |
-
if is_single_user_owner(user["username"]):
|
| 57 |
return ALL_PERMISSIONS
|
| 58 |
return permissions_for_role(user["role"])
|
| 59 |
|
|
@@ -93,6 +93,6 @@ def delete_session(db: Database, token: str) -> None:
|
|
| 93 |
|
| 94 |
|
| 95 |
def has_permission(user: dict[str, Any], permission: str) -> bool:
|
| 96 |
-
if is_single_user_owner(user["username"]):
|
| 97 |
return True
|
| 98 |
return permission in ROLE_PERMISSIONS.get(user.get("role", "reviewer"), set())
|
|
|
|
| 7 |
|
| 8 |
from pymongo.database import Database
|
| 9 |
|
| 10 |
+
from data_studio.auth import authenticate_user, get_active_user, is_admin_username, is_single_user_owner, serialize_user
|
| 11 |
from data_studio.utils import utc_now
|
| 12 |
|
| 13 |
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def permissions_for_user(user: dict[str, Any]) -> list[str]:
|
| 56 |
+
if is_single_user_owner(user["username"]) or is_admin_username(user["username"]):
|
| 57 |
return ALL_PERMISSIONS
|
| 58 |
return permissions_for_role(user["role"])
|
| 59 |
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
def has_permission(user: dict[str, Any], permission: str) -> bool:
|
| 96 |
+
if is_single_user_owner(user["username"]) or is_admin_username(user["username"]):
|
| 97 |
return True
|
| 98 |
return permission in ROLE_PERMISSIONS.get(user.get("role", "reviewer"), set())
|
frontend/app/page.tsx
CHANGED
|
@@ -124,7 +124,7 @@ export default function Home() {
|
|
| 124 |
const [tokenProgress, setTokenProgress] = useState<TokenProgress | null>(null);
|
| 125 |
const [collaboration, setCollaboration] = useState<Collaboration | null>(null);
|
| 126 |
const [section, setSection] = useState<"review" | "overview" | "admin">("review");
|
| 127 |
-
const [queue, setQueue] = useState("
|
| 128 |
const [sample, setSample] = useState<Sample | null>(null);
|
| 129 |
const [busy, setBusy] = useState(false);
|
| 130 |
const [message, setMessage] = useState("");
|
|
@@ -297,7 +297,7 @@ function ReviewWorkspace({ queue, queues, sources, sample, busy, collaboration,
|
|
| 297 |
const selectedSource = sources.find((source) => source.dataset_key === datasetKey);
|
| 298 |
const selectedQueue = selectedSource?.queue_name || queue;
|
| 299 |
const claimLabel = selectedSource ? `Claim from ${selectedSource.name}` : "Claim from all active datasets";
|
| 300 |
-
return <><header className="pageHeader"><div><span className="eyebrow">Review workspace</span><h1>Make one clear decision at a time.</h1><p>Each reviewer gets a random available sample. Token progress stays global across the whole team.</p></div><div className="headerStats">{collaboration && <div className="metricInline"><span>Total cleaned</span><strong>{collaboration.total_cleaned_samples.toLocaleString()}</strong></div>}{sample?.lease && <div className="lease">Reserved until {new Date(sample.lease.expires_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>}</div></header><section className="tableCard connectedCard"><div className="connectedHeader"><div><h2>Connected datasets</h2><p>Leave the selector empty to pool across every active dataset
|
| 301 |
}
|
| 302 |
|
| 303 |
function ReviewEditor({ sample, busy, onSubmit }: { sample: Sample; busy: boolean; onSubmit: (action: string, editor: EditorState) => void }) {
|
|
|
|
| 124 |
const [tokenProgress, setTokenProgress] = useState<TokenProgress | null>(null);
|
| 125 |
const [collaboration, setCollaboration] = useState<Collaboration | null>(null);
|
| 126 |
const [section, setSection] = useState<"review" | "overview" | "admin">("review");
|
| 127 |
+
const [queue, setQueue] = useState("all");
|
| 128 |
const [sample, setSample] = useState<Sample | null>(null);
|
| 129 |
const [busy, setBusy] = useState(false);
|
| 130 |
const [message, setMessage] = useState("");
|
|
|
|
| 297 |
const selectedSource = sources.find((source) => source.dataset_key === datasetKey);
|
| 298 |
const selectedQueue = selectedSource?.queue_name || queue;
|
| 299 |
const claimLabel = selectedSource ? `Claim from ${selectedSource.name}` : "Claim from all active datasets";
|
| 300 |
+
return <><header className="pageHeader"><div><span className="eyebrow">Review workspace</span><h1>Make one clear decision at a time.</h1><p>Each reviewer gets a random available sample. Token progress stays global across the whole team.</p></div><div className="headerStats">{collaboration && <div className="metricInline"><span>Total cleaned</span><strong>{collaboration.total_cleaned_samples.toLocaleString()}</strong></div>}{sample?.lease && <div className="lease">Reserved until {new Date(sample.lease.expires_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div>}</div></header><section className="tableCard connectedCard"><div className="connectedHeader"><div><h2>Connected datasets</h2><p>Leave the selector empty to pool across every active dataset.</p></div><label>Active dataset<select value={datasetKey} onChange={(e) => setDatasetKey(e.target.value)}><option value="">All active datasets</option>{sources.map((source) => <option key={source.dataset_key} value={source.dataset_key}>{source.name}</option>)}</select></label></div><div className="connectedList">{sources.map((source) => <button key={source.dataset_key} type="button" className={`connectedChip${source.dataset_key === datasetKey ? " active" : ""}`} onClick={() => setDatasetKey(source.dataset_key)}><strong>{source.name}</strong><small>{source.dataset_key}</small></button>)}</div></section>{sample ? <ReviewEditor key={sample.sample_id} sample={sample} busy={busy} onSubmit={onSubmit} /> : <div className="emptyState"><div>✓</div><h2>Ready for the next item?</h2><p>{selectedSource ? `${selectedSource.name} is active.` : "All active datasets are pooled together."}</p><button className="primary" onClick={() => onClaim(datasetKey || undefined)} disabled={busy}>{busy ? "Finding an item..." : claimLabel}</button></div>}</>;
|
| 301 |
}
|
| 302 |
|
| 303 |
function ReviewEditor({ sample, busy, onSubmit }: { sample: Sample; busy: boolean; onSubmit: (action: string, editor: EditorState) => void }) {
|