KPrashanth commited on
Commit
c0bfa91
·
verified ·
1 Parent(s): 578769d

Deploy Rachana Data Studio

Browse files
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
- queue_name = st.selectbox(
290
- "Queue",
291
- ["pure_telugu", "translation_tel_eng", "transliteration"],
292
- )
293
- queue_stats = counts.get(queue_name, {})
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(db, queue_name, user["username"])
301
- if sample is None:
302
- st.info("No pending samples for this queue.")
303
- return
 
 
 
 
 
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("pure_telugu");
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 in this queue.</p></div><label>Active dataset<select value={datasetKey} onChange={(e) => { const next = e.target.value; setDatasetKey(next); const nextSource = sources.find((source) => source.dataset_key === next); if (nextSource) onQueue(nextSource.queue_name); }}><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); onQueue(source.queue_name); }}><strong>{source.name}</strong><small>{source.dataset_key}</small></button>)}</div></section><div className="queueTabs">{QUEUES.map((item) => <button key={item.id} className={queue === item.id ? "active" : ""} onClick={() => onQueue(item.id)}><strong>{item.label}</strong><span>{queues[item.id]?.pending || 0} pending</span></button>)}</div>{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 mapped to ${selectedQueue}.` : QUEUES.find((q) => q.id === queue)?.description}</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 }) {
 
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 }) {