Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ build-essential \
5
+ cmake \
6
+ libnetcdf-dev \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ RUN useradd -m -u 1000 user
10
+ USER user
11
+
12
+ ENV HOME=/home/user \
13
+ PATH=/home/user/.local/bin:$PATH
14
+
15
+ WORKDIR $HOME/app
16
+
17
+ RUN pip install --no-cache-dir --upgrade pip
18
+ COPY --chown=user . $HOME/app
19
+ RUN pip install -r requirements.txt
20
+
21
+ EXPOSE 7860
22
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
23
+
24
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -3,13 +3,15 @@ title: MecCog Challenge
3
  emoji: 🔋
4
  colorFrom: purple
5
  colorTo: green
6
- sdk: gradio
7
- sdk_version: "5.50.0"
8
  pinned: false
9
  hf_oauth: true
10
  hf_oauth_expiration_minutes: 480
11
  hf_oauth_scopes:
12
- - email
 
 
 
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
3
  emoji: 🔋
4
  colorFrom: purple
5
  colorTo: green
6
+ sdk: docker
 
7
  pinned: false
8
  hf_oauth: true
9
  hf_oauth_expiration_minutes: 480
10
  hf_oauth_scopes:
11
+ - read-repos
12
+ - write-repos
13
+ - manage-repos
14
+ - inference-api
15
  ---
16
 
17
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
about.py CHANGED
@@ -5,12 +5,12 @@ from huggingface_hub import HfApi
5
  load_dotenv()
6
 
7
  TOKEN = os.environ.get("HF_TOKEN")
8
- CACHE_PATH = os.getenv("HF_HOME", ".")
9
  API = HfApi(token=TOKEN)
10
 
11
- CHALLENGE_NAME = "MecCogChallenge"
12
- _ORGANIZATION = "MecCog"
13
 
14
- SUBMISSIONS_REPO = f"{_ORGANIZATION}/{CHALLENGE_NAME}"
15
- RESULTS_REPO = f"{_ORGANIZATION}/{CHALLENGE_NAME}"
16
- REGISTRATION_REPO = f"{_ORGANIZATION}/{CHALLENGE_NAME}"
 
5
  load_dotenv()
6
 
7
  TOKEN = os.environ.get("HF_TOKEN")
8
+ CACHE_PATH=os.getenv("HF_HOME", ".")
9
  API = HfApi(token=TOKEN)
10
 
11
+ CHALLENGE_NAME="MecCogChallenge"
12
+ _ORGANIZATION="MecCog"
13
 
14
+ SUBMISSIONS_REPO = f'{_ORGANIZATION}/{CHALLENGE_NAME}Submissions'
15
+ RESULTS_REPO = f'{_ORGANIZATION}/{CHALLENGE_NAME}Results'
16
+ REGISTRATION_REPO = f'{_ORGANIZATION}/{CHALLENGE_NAME}Registrations'
app.py CHANGED
@@ -7,29 +7,16 @@ from about import CHALLENGE_NAME
7
 
8
  PROBLEM_TYPES = ["hypothesis 1", "hypothesis 2", "hypothesis 3"]
9
 
10
-
11
  def gradio_interface() -> gr.Blocks:
12
  with gr.Blocks() as demo:
13
  gr.Markdown(f"## Welcome to the {CHALLENGE_NAME}!")
14
-
15
- active_tab = gr.BrowserState(0)
16
-
17
- with gr.Tabs(elem_classes="tab-buttons") as tabs:
18
  challenge_page.get_description(gr=gr)
19
- registration_page.get_registration_page(gr=gr, demo=demo)
20
- submission_page.get_submission_page(gr=gr, demo=demo)
21
  faq.get_faq(gr=gr)
22
  leaderboard_page.get_leaderboard(gr=gr)
23
-
24
- def restore_tab(idx):
25
- return gr.Tabs(selected=idx)
26
-
27
- def save_tab(evt: gr.SelectData):
28
- return evt.index
29
-
30
- tabs.select(fn=save_tab, inputs=None, outputs=[active_tab])
31
- demo.load(fn=restore_tab, inputs=[active_tab], outputs=[tabs])
32
-
33
  return demo
34
 
35
 
 
7
 
8
  PROBLEM_TYPES = ["hypothesis 1", "hypothesis 2", "hypothesis 3"]
9
 
 
10
  def gradio_interface() -> gr.Blocks:
11
  with gr.Blocks() as demo:
12
  gr.Markdown(f"## Welcome to the {CHALLENGE_NAME}!")
13
+ with gr.Tabs(elem_classes="tab-buttons"):
 
 
 
14
  challenge_page.get_description(gr=gr)
15
+ registration_page.get_registration_page(gr=gr)
16
+ submission_page.get_submission_page(gr=gr)
17
  faq.get_faq(gr=gr)
18
  leaderboard_page.get_leaderboard(gr=gr)
19
+
 
 
 
 
 
 
 
 
 
20
  return demo
21
 
22
 
components/challenge_page.py CHANGED
@@ -2,132 +2,19 @@ from about import CHALLENGE_NAME
2
 
3
  TAB_TITLE = "❔About"
4
 
5
- CHALLENGE_DESCRIPTION_1 = f"""
6
  ## About This Challenge
7
 
8
- **Welcome to the MecCog Challenge**, a collaborative effort to map the mechanisms by which the APOE4 genetic variant increases the risk of Alzheimer's disease.
9
 
10
- ### What You'll Do
11
 
12
- Your task is to **find relevant research papers and extract key experimental findings** that relate to specific APOE4-Alzheimer's hypothesis you decide to select.
13
 
14
- For each hypothesis, you will:
15
- 1. **Search the scientific literature** (PubMed, preprints, databases, web sources)
16
- 2. **Identify papers** that bear on your specific mechanism
17
- 3. **Extract experimental findings** from those papers (e.g., measurements, effects, experimental conditions)
18
- 4. **Record everything** in a structured spreadsheet that we'll validate
19
-
20
- This is Step 1 of a multi-phase challenge. Later phases will ask you to assess evidence, estimate mechanism confidence, and design experiments — but for now, we focus on **finding and documenting what is already known**.
21
-
22
- ---
23
-
24
- ## The Hypotheses
25
-
26
- Hypotheses will be released in batches starting **September 2026**. Check for new communications.
27
-
28
- **Focus on the hypothesis you decide to work on.** Don't catalog Alzheimer's mechanisms in general — we need findings that directly test or inform *the specific hypothesis you picked*.
29
-
30
-
31
- ---
32
-
33
- ## Challenge Rules
34
-
35
- ### What You Must Do
36
-
37
- ✓ **Search thoroughly** — use PubMed, preprint servers (bioRxiv/medRxiv), Google Scholar, databases, and any sources you think are relevant.
38
-
39
- ✓ **Extract precise findings** — include:
40
- - A one-sentence description of each finding
41
- - A direct quote from the paper (or mark `N/A` if not applicable)
42
- - The experimental system tested (e.g., "primary mouse microglia", "human iPSC-derived neurons")
43
- - Where in the paper the finding appears (figure panel, table, or text excerpt)
44
- - Statistical details if available (effect size, P value, sample size)
45
-
46
- ✓ **Fill the submission spreadsheet** — use the official template provided. Every field must have a value or the literal `N/A` (never leave blanks).
47
-
48
- ✓ **Record metadata correctly** — include the DOI, PubMed ID (if applicable), and source type for each paper.
49
-
50
- ✓ **Test your submission locally** — click on the "Validate" button, on the submission page. You will not be able to submit your submission if the validation failed.
51
-
52
- ### What's Out of Bounds
53
-
54
- ✗ **Don't pad your sheet** — include only papers and findings truly relevant to *your hypothesis*, not related Alzheimer's research in general.
55
-
56
- ✗ **Don't fabricate** — every finding must exist in the paper at the location you cite. Misattributions will be caught and disqualify your submission.
57
-
58
- ✗ **Don't skip ahead** — this is Step 1 only. Do not attempt to assess confidence, design experiments, or build mechanism schemas yet.
59
-
60
- ✗ **Don't use fake identifiers** — DOIs and PubMed IDs must correspond to real, published sources.
61
-
62
- ### Submission Format
63
-
64
- One spreadsheet per hypothesis. Download the official template from shared resources:
65
- """
66
-
67
- CHALLENGE_DESCRIPTION_2 = """
68
-
69
- **Spreadsheet structure:**
70
- - **Row 1:** Column labels (provided in template)
71
- - **Row 2:** Your assigned hypothesis
72
- - **Row 3 onward:** One block per paper (paper row + finding rows)
73
-
74
- **Key columns:**
75
- - **DOI** — unique identifier for the source
76
- - **PubMed ID** — if applicable (digits only)
77
- - **Paper ID** — sequential (`P1`, `P2`, etc., in order of relevance)
78
- - **Finding ID** — (`P1.F1`, `P1.F2`, etc.)
79
- - **Finding description** — plain English, one sentence
80
- - **Finding quote** — exact text from the paper, or `N/A`
81
- - **Finding summary** — structured notation (e.g., `(APOE4) -> (↓ phagocytosis)`) or `N/A`
82
- - **Experimental system** — what cells/model was tested? (e.g., "mouse primary microglia")
83
- - **Data location** — figure panel (e.g., `Fig4C`), table number, or exact sentence
84
- - **Effect size, P value, sample size** — if reported; otherwise `N/A`
85
-
86
- ### How You'll Be Evaluated
87
-
88
- **Your submission will be assessed on:**
89
-
90
- 1. **Format validity** — Does it pass the automated validator? (Required to submit your submission)
91
- 2. **Paper relevance** — Are the papers you found actually related to your hypothesis?
92
- 3. **Finding accuracy** — Do the extracted findings genuinely appear where you cite them?
93
- 4. **Consensus** — Do independent submissions (from other humans and agents) converge on the same papers and findings?
94
- 5. **Completeness** — How thorough was your search? Did you miss obvious sources?
95
- 6. **Expert review** — Specialist assessment of whether your extraction captures the key evidence.
96
-
97
- **No single "right answer" exists yet** — this is open scientific discovery. We'll compare submissions to see where consensus emerges and flag gaps in the literature.
98
-
99
- ---
100
-
101
- ## How to Get Started
102
-
103
- 1. Check the proposed hypothesis
104
- 2. **Download the submission template** `submission_template.xlsx` (see above).
105
- 3. **Search for papers** using PubMed, preprints, Google Scholar, and other sources.
106
- 4. **Extract findings** and fill in the spreadsheet as you go.
107
- 5. **Validate** your submission before uploading.
108
- 6. **Upload your submission** on the submission page.
109
-
110
- ---
111
-
112
- ## Timeline & Questions
113
-
114
- - **Step 1 (this task) launches:** September 2026
115
- - **Hypotheses released:**
116
- - **Later phases:** Evidence assessment, mechanism confidence, experimental design (dates TBA)
117
- - **Questions?** Check the FAQ tab or contact the organizers — organizers and community are here to help.
118
-
119
- ---
120
-
121
- **Thank you for contributing to MecCog. Together, we're building a map of APOE4 mechanism.** 🧠
122
  """
123
 
124
 
125
  def get_description(gr):
126
- with gr.TabItem(TAB_TITLE, elem_id="boundary-benchmark-tab-table", id=0):
127
- gr.Markdown(CHALLENGE_DESCRIPTION_1)
128
- gr.DownloadButton(
129
- label="📥 Download Template",
130
- value="components/submission/submission_template.xlsx",
131
- variant="secondary",
132
- )
133
- gr.Markdown(CHALLENGE_DESCRIPTION_2)
 
2
 
3
  TAB_TITLE = "❔About"
4
 
5
+ CHALLENGE_DESCRIPTION = f"""
6
  ## About This Challenge
7
 
8
+ **Welcome to the {CHALLENGE_NAME}**, a community-driven effort to....
9
 
10
+ [ CHALLENGE DESCRIPTION ]
11
 
12
+ [ LIST HYPOTHESIS ]
13
 
14
+ [ CHALLENGE RULES ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  """
16
 
17
 
18
  def get_description(gr):
19
+ with gr.TabItem(TAB_TITLE, elem_id="boundary-benchmark-tab-table"):
20
+ gr.Markdown(CHALLENGE_DESCRIPTION)
 
 
 
 
 
 
components/registration/config.py CHANGED
@@ -38,15 +38,13 @@ TEAMS_FILE_NAME = "teams.csv"
38
  # Schema – column names and dtypes for each dataset
39
  # ---------------------------------------------------------------------------
40
  PARTICIPANT_COLUMNS: list[str] = [
41
- "username",
42
  "email",
43
  "name",
44
  "affiliation",
45
  "discord",
46
  "self_description",
47
- "registration_status",
48
- "team_name",
49
- "is_leader",
50
  "registered_at",
51
  "updated_at",
52
  ]
@@ -54,9 +52,9 @@ PARTICIPANT_COLUMNS: list[str] = [
54
  TEAM_COLUMNS: list[str] = [
55
  "team_name",
56
  "team_description",
57
- "leader_username",
58
- "members_username", # JSON-encoded list[str]
59
  "second_team_reason",
60
  "created_at",
61
  "updated_at",
62
- ]
 
38
  # Schema – column names and dtypes for each dataset
39
  # ---------------------------------------------------------------------------
40
  PARTICIPANT_COLUMNS: list[str] = [
 
41
  "email",
42
  "name",
43
  "affiliation",
44
  "discord",
45
  "self_description",
46
+ "needs_manual_review",
47
+ "team_memberships",
 
48
  "registered_at",
49
  "updated_at",
50
  ]
 
52
  TEAM_COLUMNS: list[str] = [
53
  "team_name",
54
  "team_description",
55
+ "leader_email",
56
+ "member_emails", # JSON-encoded list[str]
57
  "second_team_reason",
58
  "created_at",
59
  "updated_at",
60
+ ]
components/registration/registration_page.py CHANGED
@@ -4,7 +4,6 @@ import json
4
  import logging
5
  from datetime import datetime, timezone
6
 
7
- import gradio
8
  import pandas as pd
9
 
10
  from about import REGISTRATION_REPO
@@ -17,18 +16,13 @@ from components.registration.config import (
17
  TEAM_COLUMNS,
18
  )
19
  from components.registration.utils import (
 
20
  validate_email_domain,
21
  is_institutional_email,
22
  validate_email_format,
23
  get_user_teams,
24
- rename_user_teams,
25
- )
26
- from components.utils import (
27
- push_data_to_dataset,
28
- load_data_from_dataset,
29
- get_team,
30
- prefill_user_info,
31
  )
 
32
 
33
  logger = logging.getLogger(__name__)
34
 
@@ -38,12 +32,10 @@ def _validate_registration_input(data: dict[str, str]) -> list[str]:
38
  errors: list[str] = []
39
  email = (data.get("email") or "").strip()
40
  name = (data.get("name") or "").strip()
41
- username = (data.get("username") or "").strip()
42
  affiliation = (data.get("affiliation") or "").strip()
43
  role = (data.get("role") or "").strip()
44
 
45
- if not name or not username:
46
- return ["❌ You must be logged in with your HuggingFace account to register."]
47
  if not email:
48
  errors.append("An email address is required.")
49
  elif not validate_email_format(email):
@@ -87,7 +79,6 @@ def register_participant(data: dict) -> tuple[bool, str]:
87
  Persist a participant registration to HuggingFace datasets.
88
 
89
  ``data`` keys (all str unless noted):
90
- username – always required, unique from huggingface account
91
  email – always required
92
  name – always required
93
  affiliation – always required
@@ -107,19 +98,18 @@ def register_participant(data: dict) -> tuple[bool, str]:
107
  if errors:
108
  return False, "❌ Validation failed:\n" + "\n".join(f" • {e}" for e in errors)
109
 
110
- username = data["username"].strip() # unique
111
-
112
- email = data["email"].strip()
113
- name = data["name"].strip()
114
- affiliation = data["affiliation"].strip()
115
- role = data["role"].strip()
116
  self_description = (data.get("self_description") or "").strip()
117
  is_new_team: bool = bool(data.get("is_new_team", False))
118
- team_name = data["team_name"].strip()
119
  team_description = (data.get("team_description") or "").strip()
120
  is_leader: bool = bool(data.get("is_leader", False))
121
  second_team_reason = (data.get("second_team_reason") or "").strip()
122
  institutional = is_institutional_email(email=email)
 
123
  now = datetime.now(timezone.utc).isoformat()
124
 
125
  # ── 2. Load both datasets ────────────────────────────────────────────────
@@ -143,42 +133,34 @@ def register_participant(data: dict) -> tuple[bool, str]:
143
  "❌ This team already exists. Please provide another Team name, or join an existing team. .",
144
  )
145
 
146
- # ── 3. Resolve participant row (upsert by username + team_name) ──────────
147
- existing_participant = participants_df[participants_df["username"] == username]
148
  is_new_participant = existing_participant.empty
149
 
150
- existing_in_this_team = existing_participant[
151
- existing_participant["team_name"] == team_name # fix: was "team"
152
- ]
153
- is_already_in_this_team = not existing_in_this_team.empty
154
-
155
- is_second_team = not existing_participant.empty and not is_already_in_this_team
156
-
157
- registration_status = (
158
- "Pending" if not institutional or is_second_team else "Validated"
159
- )
160
- if is_already_in_this_team:
161
- registration_status = existing_in_this_team.iloc[0].get(
162
- "registration_status", "Pending"
163
- )
164
-
165
  if is_new_participant:
 
166
  registered_at = now
167
  else:
 
 
 
168
  registered_at = existing_participant.iloc[0].get("registered_at", now)
169
- if is_second_team and not second_team_reason:
170
- return (
171
- False,
172
- "This user is already part of a team. Please provide a reason for joining a second team.",
173
- )
 
 
 
174
  # ── 4. Resolve team ──────────────────────────────────────────────────────
175
  if is_new_team:
176
  # ── 4a. Creating a new team ──────────────────────────────────────────
177
  new_team_row = {
178
  "team_name": team_name,
179
  "team_description": team_description,
180
- "leader_username": username if is_leader else "",
181
- "members_username": json.dumps([username]),
182
  "second_team_reason": second_team_reason,
183
  "created_at": now,
184
  "updated_at": now,
@@ -199,59 +181,68 @@ def register_participant(data: dict) -> tuple[bool, str]:
199
  )
200
 
201
  # Add participant to member list if not already there
202
- members_username: list[str] = json.loads(
203
- team_row.get("members_username") or "[]"
204
- )
205
- if username not in members_username:
206
- members_username.append(username)
207
 
208
  # Assign leader if requested and slot is free
209
- current_leader = (team_row.get("leader_username") or "").strip()
210
  if is_leader:
211
- if current_leader and current_leader != username:
212
  return False, (
213
  f"❌ Team '{name}' already has a designated leader "
214
  f"({current_leader}). "
215
  "Please contact the current leader to transfer leadership."
216
  )
217
- new_leader = username
218
  else:
219
  new_leader = current_leader # unchanged
220
 
221
  # Update team row in-place
222
  mask = teams_df["team_name"] == team_name
223
- teams_df.loc[mask, "members_username"] = json.dumps(members_username)
224
- teams_df.loc[mask, "leader_username"] = new_leader
225
  teams_df.loc[mask, "updated_at"] = now
226
  action_msg = f"Joined existing team '{team_name}' ({team_name})."
227
 
228
- # ── 5. Upsert participant row (keyed on username + team_name) ────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  participant_row = {
230
  "email": email,
231
  "name": name,
232
- "username": username,
233
  "affiliation": affiliation,
234
  "role": role,
235
  "self_description": self_description,
236
- "registration_status": registration_status,
237
- "team_name": team_name,
238
- "is_leader": is_leader,
239
  "registered_at": registered_at,
240
  "updated_at": now,
241
  }
242
 
243
- if is_already_in_this_team:
244
- mask = (participants_df["username"] == username) & (
245
- participants_df["team_name"] == team_name
246
- )
247
- for col, val in participant_row.items():
248
- participants_df.loc[mask, col] = val
249
- else:
250
  participants_df = pd.concat(
251
  [participants_df, pd.DataFrame([participant_row])], ignore_index=True
252
  )
 
 
 
253
 
254
- # ── 6. Push both datasets back to HF ────────────────────────────────────
255
  try:
256
  push_data_to_dataset(participants_df, REGISTRATION_REPO, PARTICIPANTS_FILE_NAME)
257
  push_data_to_dataset(teams_df, REGISTRATION_REPO, TEAMS_FILE_NAME)
@@ -267,38 +258,31 @@ def register_participant(data: dict) -> tuple[bool, str]:
267
  review_note = (
268
  "Your registration requires manual review because no institutional email "
269
  "was provided – you may receive a follow-up email."
270
- if registration_status == "Pending"
271
  else ""
272
  )
273
  message = (
274
- f"Registration {'created' if is_new_participant else 'updated'} for {username}. "
275
- f"{action_msg} {leader_note} {review_note}"
276
  )
277
  return True, message
278
 
279
 
280
- def get_registration_page(gr, demo):
281
- with gr.TabItem("Register", id=1):
282
  gr.Markdown(REGISTRATION_DESCRIPTION)
283
 
284
- gr.LoginButton()
285
-
286
  with gr.Row():
287
  with gr.Column():
288
- username = gr.Textbox(
289
- label="Username from your Hugging Face account *",
290
- placeholder="Fetching from HuggingFace...",
291
- interactive=False,
292
  )
293
  name = gr.Textbox(
294
- label="Full name from your Hugging Face account *",
295
- placeholder="Fetching from HuggingFace...",
296
- interactive=False,
297
  )
298
- email = gr.Textbox(
299
- label="Email *", placeholder="myemail@domain.com", interactive=True
300
- )
301
-
302
  affiliation = gr.Textbox(
303
  label="Affiliation / Institution *",
304
  placeholder="University / Company name",
@@ -343,14 +327,13 @@ def get_registration_page(gr, demo):
343
  visible=False,
344
  )
345
 
346
- def on_see_user_teams(hf_username: str):
347
- if not hf_username or not hf_username.strip():
348
  return gr.Dataframe(visible=False), gr.Markdown(
349
- "❌ You must be logged in with your HuggingFace account to see your teams.",
350
- visible=True,
351
  )
352
 
353
- teams = rename_user_teams(get_user_teams(username=hf_username))
354
 
355
  if len(teams) > 0:
356
  return gr.Dataframe(value=teams, visible=True), gr.Markdown(
@@ -363,7 +346,7 @@ def get_registration_page(gr, demo):
363
 
364
  user_team_button.click(
365
  fn=on_see_user_teams,
366
- inputs=[username],
367
  outputs=[user_teams, user_teams_empty_msg],
368
  )
369
 
@@ -409,6 +392,7 @@ def get_registration_page(gr, demo):
409
 
410
  def on_register(
411
  registration_email: str,
 
412
  registration_affiliation: str,
413
  self_desc: str,
414
  is_new_team: bool,
@@ -417,19 +401,15 @@ def get_registration_page(gr, demo):
417
  role: str,
418
  is_leader: bool,
419
  second_reason: str,
420
- oauth_profile: gradio.OAuthProfile | None,
421
  ):
422
- if oauth_profile is None:
423
- return gr.update(
424
- value="❌ You must be logged in with your HuggingFace account to register.",
425
- visible=True,
426
- )
427
-
428
- errors: list[str] = []
429
 
430
  if not registration_email.strip():
431
  errors.append("Email is required.")
432
 
 
 
 
433
  if not registration_affiliation.strip():
434
  errors.append("Affiliation is required.")
435
  if registration_email.strip() and not validate_email_domain(
@@ -453,9 +433,8 @@ def get_registration_page(gr, demo):
453
  return gr.update(value=msg, visible=True)
454
 
455
  data = {
456
- "username": oauth_profile.username,
457
- "name": oauth_profile.name,
458
  "email": registration_email,
 
459
  "affiliation": registration_affiliation,
460
  "self_description": self_desc,
461
  "is_new_team": is_new_team,
@@ -478,6 +457,7 @@ def get_registration_page(gr, demo):
478
  fn=on_register,
479
  inputs=[
480
  email,
 
481
  affiliation,
482
  description,
483
  is_new_team_checkbox,
@@ -489,9 +469,3 @@ def get_registration_page(gr, demo):
489
  ],
490
  outputs=[registration_status],
491
  )
492
-
493
- demo.load(
494
- fn=prefill_user_info,
495
- inputs=[],
496
- outputs=[username, name, email],
497
- )
 
4
  import logging
5
  from datetime import datetime, timezone
6
 
 
7
  import pandas as pd
8
 
9
  from about import REGISTRATION_REPO
 
16
  TEAM_COLUMNS,
17
  )
18
  from components.registration.utils import (
19
+ get_team_membership_entry,
20
  validate_email_domain,
21
  is_institutional_email,
22
  validate_email_format,
23
  get_user_teams,
 
 
 
 
 
 
 
24
  )
25
+ from components.utils import push_data_to_dataset, load_data_from_dataset, get_team
26
 
27
  logger = logging.getLogger(__name__)
28
 
 
32
  errors: list[str] = []
33
  email = (data.get("email") or "").strip()
34
  name = (data.get("name") or "").strip()
 
35
  affiliation = (data.get("affiliation") or "").strip()
36
  role = (data.get("role") or "").strip()
37
 
38
+ # Email is always required
 
39
  if not email:
40
  errors.append("An email address is required.")
41
  elif not validate_email_format(email):
 
79
  Persist a participant registration to HuggingFace datasets.
80
 
81
  ``data`` keys (all str unless noted):
 
82
  email – always required
83
  name – always required
84
  affiliation – always required
 
98
  if errors:
99
  return False, "❌ Validation failed:\n" + "\n".join(f" • {e}" for e in errors)
100
 
101
+ email = data["email"].strip().lower()
102
+ name = data["name"].strip().lower()
103
+ affiliation = data["affiliation"].strip().lower()
104
+ role = data["role"].strip().lower()
 
 
105
  self_description = (data.get("self_description") or "").strip()
106
  is_new_team: bool = bool(data.get("is_new_team", False))
107
+ team_name = data["team_name"].strip().lower()
108
  team_description = (data.get("team_description") or "").strip()
109
  is_leader: bool = bool(data.get("is_leader", False))
110
  second_team_reason = (data.get("second_team_reason") or "").strip()
111
  institutional = is_institutional_email(email=email)
112
+ needs_manual_review = not institutional
113
  now = datetime.now(timezone.utc).isoformat()
114
 
115
  # ── 2. Load both datasets ────────────────────────────────────────────────
 
133
  "❌ This team already exists. Please provide another Team name, or join an existing team. .",
134
  )
135
 
136
+ # ── 3. Resolve participant row (upsert) ──────────────────────────────────
137
+ existing_participant = participants_df[participants_df["email"] == email]
138
  is_new_participant = existing_participant.empty
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  if is_new_participant:
141
+ existing_memberships: list[dict] = []
142
  registered_at = now
143
  else:
144
+ existing_memberships = json.loads(
145
+ existing_participant.iloc[0].get("team_memberships") or "[]"
146
+ )
147
  registered_at = existing_participant.iloc[0].get("registered_at", now)
148
+
149
+ already_has_team = len(existing_memberships) > 0
150
+ if already_has_team and not second_team_reason:
151
+ return (
152
+ False,
153
+ "❌ This user is already part of a team. Please provide a reason for joining a second team.",
154
+ )
155
+
156
  # ── 4. Resolve team ──────────────────────────────────────────────────────
157
  if is_new_team:
158
  # ── 4a. Creating a new team ──────────────────────────────────────────
159
  new_team_row = {
160
  "team_name": team_name,
161
  "team_description": team_description,
162
+ "leader_email": email if is_leader else "",
163
+ "member_emails": json.dumps([email]),
164
  "second_team_reason": second_team_reason,
165
  "created_at": now,
166
  "updated_at": now,
 
181
  )
182
 
183
  # Add participant to member list if not already there
184
+ member_emails: list[str] = json.loads(team_row.get("member_emails") or "[]")
185
+ if email not in member_emails:
186
+ member_emails.append(email)
 
 
187
 
188
  # Assign leader if requested and slot is free
189
+ current_leader = (team_row.get("leader_email") or "").strip()
190
  if is_leader:
191
+ if current_leader and current_leader != email:
192
  return False, (
193
  f"❌ Team '{name}' already has a designated leader "
194
  f"({current_leader}). "
195
  "Please contact the current leader to transfer leadership."
196
  )
197
+ new_leader = email
198
  else:
199
  new_leader = current_leader # unchanged
200
 
201
  # Update team row in-place
202
  mask = teams_df["team_name"] == team_name
203
+ teams_df.loc[mask, "member_emails"] = json.dumps(member_emails)
204
+ teams_df.loc[mask, "leader_email"] = new_leader
205
  teams_df.loc[mask, "updated_at"] = now
206
  action_msg = f"Joined existing team '{team_name}' ({team_name})."
207
 
208
+ # ── 5. Build updated membership list for participant ─────────────────────
209
+ # If participant is already a member of this team, update is_leader in place
210
+ existing_team_names = [m["team_name"] for m in existing_memberships]
211
+ if team_name in existing_team_names:
212
+ updated_memberships = [
213
+ get_team_membership_entry(
214
+ team_name=m["team_name"],
215
+ is_leader=is_leader if m["team_name"] == team_name else m["is_leader"],
216
+ )
217
+ for m in existing_memberships
218
+ ]
219
+ else:
220
+ updated_memberships = existing_memberships + [
221
+ get_team_membership_entry(team_name=team_name, is_leader=is_leader)
222
+ ]
223
+
224
+ # ── 6. Upsert participant row ────────────────────────────────────────────
225
  participant_row = {
226
  "email": email,
227
  "name": name,
 
228
  "affiliation": affiliation,
229
  "role": role,
230
  "self_description": self_description,
231
+ "needs_manual_review": needs_manual_review,
232
+ "team_memberships": json.dumps(updated_memberships),
 
233
  "registered_at": registered_at,
234
  "updated_at": now,
235
  }
236
 
237
+ if is_new_participant:
 
 
 
 
 
 
238
  participants_df = pd.concat(
239
  [participants_df, pd.DataFrame([participant_row])], ignore_index=True
240
  )
241
+ else:
242
+ for col, val in participant_row.items():
243
+ participants_df.loc[participants_df["email"] == email, col] = val
244
 
245
+ # ── 7. Push both datasets back to HF ────────────────────────────────────
246
  try:
247
  push_data_to_dataset(participants_df, REGISTRATION_REPO, PARTICIPANTS_FILE_NAME)
248
  push_data_to_dataset(teams_df, REGISTRATION_REPO, TEAMS_FILE_NAME)
 
258
  review_note = (
259
  "Your registration requires manual review because no institutional email "
260
  "was provided – you may receive a follow-up email."
261
+ if needs_manual_review
262
  else ""
263
  )
264
  message = (
265
+ f"Registration {'created' if is_new_participant else 'updated'} for {email}. "
266
+ f"{action_msg}{leader_note}{review_note}"
267
  )
268
  return True, message
269
 
270
 
271
+ def get_registration_page(gr):
272
+ with gr.TabItem("Register"):
273
  gr.Markdown(REGISTRATION_DESCRIPTION)
274
 
 
 
275
  with gr.Row():
276
  with gr.Column():
277
+ email = gr.Textbox(
278
+ label="Institutional / Company Email *",
279
+ placeholder="you@university.edu or you@company.com",
280
+ info="Academic (.edu, .ac.*) or company domains are accepted automatically.",
281
  )
282
  name = gr.Textbox(
283
+ label="Full Name *",
284
+ placeholder="Jane Smith",
 
285
  )
 
 
 
 
286
  affiliation = gr.Textbox(
287
  label="Affiliation / Institution *",
288
  placeholder="University / Company name",
 
327
  visible=False,
328
  )
329
 
330
+ def on_see_user_teams(user_email: str):
331
+ if not user_email or not user_email.strip():
332
  return gr.Dataframe(visible=False), gr.Markdown(
333
+ "❌ Please enter an email address.", visible=True
 
334
  )
335
 
336
+ teams = get_user_teams(email=user_email)
337
 
338
  if len(teams) > 0:
339
  return gr.Dataframe(value=teams, visible=True), gr.Markdown(
 
346
 
347
  user_team_button.click(
348
  fn=on_see_user_teams,
349
+ inputs=[email],
350
  outputs=[user_teams, user_teams_empty_msg],
351
  )
352
 
 
392
 
393
  def on_register(
394
  registration_email: str,
395
+ registration_name: str,
396
  registration_affiliation: str,
397
  self_desc: str,
398
  is_new_team: bool,
 
401
  role: str,
402
  is_leader: bool,
403
  second_reason: str,
 
404
  ):
405
+ errors = []
 
 
 
 
 
 
406
 
407
  if not registration_email.strip():
408
  errors.append("Email is required.")
409
 
410
+ if not registration_name.strip():
411
+ errors.append("Full name is required.")
412
+
413
  if not registration_affiliation.strip():
414
  errors.append("Affiliation is required.")
415
  if registration_email.strip() and not validate_email_domain(
 
433
  return gr.update(value=msg, visible=True)
434
 
435
  data = {
 
 
436
  "email": registration_email,
437
+ "name": registration_name,
438
  "affiliation": registration_affiliation,
439
  "self_description": self_desc,
440
  "is_new_team": is_new_team,
 
457
  fn=on_register,
458
  inputs=[
459
  email,
460
+ name,
461
  affiliation,
462
  description,
463
  is_new_team_checkbox,
 
469
  ],
470
  outputs=[registration_status],
471
  )
 
 
 
 
 
 
components/registration/utils.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import logging
2
  import re
3
 
@@ -9,21 +10,17 @@ from components.utils import load_data_from_dataset
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
-
13
  def validate_email_domain(email: str) -> bool:
14
  """Return True if email belongs to an academic or registered company domain."""
15
- academic_tlds = [
16
- ".edu",
17
- ".ac.uk",
18
- ".ac.",
19
- "university",
20
- "univ.",
21
- "institute",
22
- ".gov",
23
- ]
24
  return any(token in email.lower() for token in academic_tlds)
25
 
26
 
 
 
 
 
 
27
  def validate_email_format(email: str) -> bool:
28
  """
29
  Validate the email format.
@@ -40,35 +37,33 @@ def is_institutional_email(email: str) -> bool:
40
  return any(p in email.lower() for p in patterns)
41
 
42
 
43
- def get_user_teams(username: str) -> pd.DataFrame:
44
- """
45
- Return the teams associated with a given username.
46
 
47
- :param username:The unique hf username to query.
48
- :return: The teams associated with a given username as a DataFrame,
49
- with human-readable columns for role and registration status.
 
 
 
 
50
  """
51
- EMPTY_RESULT = pd.DataFrame(
52
- columns=["team_name", "is_leader", "registration_status"]
53
- )
54
 
55
- participants_df = load_data_from_dataset(
56
- REGISTRATION_REPO, PARTICIPANTS_FILE_NAME, PARTICIPANT_COLUMNS
57
- )
58
- if participants_df.empty:
59
- return EMPTY_RESULT
60
 
61
- existing_participant = participants_df[participants_df["username"] == username]
62
  if existing_participant.empty:
63
- return EMPTY_RESULT
64
 
65
- return existing_participant[["team_name", "is_leader", "registration_status"]]
 
 
66
 
 
 
67
 
68
- def rename_user_teams(result: pd.DataFrame) -> pd.DataFrame:
69
- result["role"] = result["is_leader"].map(
70
- {True: "Leader", False: "Member", "True": "Leader", "False": "Member"}
71
- )
72
- return result[["team_name", "role", "registration_status"]].rename(
73
- columns={"team_name": "Team Name"}
74
- )
 
1
+ import json
2
  import logging
3
  import re
4
 
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
 
13
  def validate_email_domain(email: str) -> bool:
14
  """Return True if email belongs to an academic or registered company domain."""
15
+ academic_tlds = [".edu", ".ac.uk", ".ac.", "university", "univ.", "institute", ".gov"]
 
 
 
 
 
 
 
 
16
  return any(token in email.lower() for token in academic_tlds)
17
 
18
 
19
+ def get_team_membership_entry(team_name: str, is_leader: bool) -> dict[str, str | bool]:
20
+ """Return a dictionary with information about a team."""
21
+ return {"team_name": team_name, "is_leader": is_leader}
22
+
23
+
24
  def validate_email_format(email: str) -> bool:
25
  """
26
  Validate the email format.
 
37
  return any(p in email.lower() for p in patterns)
38
 
39
 
 
 
 
40
 
41
+ def _participant_team_names(participant_row: pd.Series) -> list[str]:
42
+ """Return list of team_name the participant already belongs to."""
43
+ memberships = json.loads(participant_row.get("team_memberships") or "[]")
44
+ return [m["team_name"] for m in memberships]
45
+
46
+
47
+ def get_user_teams(email: str) -> pd.DataFrame:
48
  """
49
+ Return the teams associated with a given email.
 
 
50
 
51
+ :param email: The email address to query.
52
+ :return: The teams associated with a given email as a DataFrame.
53
+ """
54
+ participants_df = load_data_from_dataset(REGISTRATION_REPO, PARTICIPANTS_FILE_NAME, PARTICIPANT_COLUMNS)
55
+ existing_participant = participants_df[participants_df["email"] == email]
56
 
 
57
  if existing_participant.empty:
58
+ return pd.DataFrame(columns=["team_name", "role"])
59
 
60
+ existing_teams = json.loads(
61
+ existing_participant.iloc[0].get("team_memberships") or "[]"
62
+ )
63
 
64
+ if not existing_teams:
65
+ return pd.DataFrame(columns=["team_name", "role"])
66
 
67
+ df = pd.DataFrame(existing_teams)
68
+ df["role"] = df["is_leader"].apply(lambda x: "Leader" if x else "Member")
69
+ return df[["team_name", "role"]]
 
 
 
 
components/submission/config.py CHANGED
@@ -23,10 +23,10 @@ You are allowed to submit up to {max} results per hypothesis.
23
  SUBMISSION_COLUMNS = [
24
  "submission_id",
25
  "team_name",
26
- "submission_username",
27
  "created_at",
28
  "hypothesis",
29
  "submission_note",
30
  "method",
31
  "file_path",
32
- ]
 
23
  SUBMISSION_COLUMNS = [
24
  "submission_id",
25
  "team_name",
26
+ "submission_email",
27
  "created_at",
28
  "hypothesis",
29
  "submission_note",
30
  "method",
31
  "file_path",
32
+ ]
components/submission/submission_page.py CHANGED
@@ -3,10 +3,8 @@ from components.submission.utils import (
3
  get_team_submission_count,
4
  submit_prediction,
5
  validate_user_registration,
6
- validate_user_team_registration,
7
  )
8
- from components.submission.validate_submission import validate
9
- from components.utils import check_team_has_leader, prefill_user_info
10
  from components.submission.config import (
11
  MAX_SUBMISSIONS_PER_HYPOTHESIS,
12
  SUBMISSION_PAGE_DESCRIPTION,
@@ -14,8 +12,18 @@ from components.submission.config import (
14
  )
15
 
16
 
17
- def get_submission_page(gr, demo):
18
- with gr.TabItem("✉️ Submit", id=2):
 
 
 
 
 
 
 
 
 
 
19
 
20
  # ── Section header ──────────────────────────────────────────────────
21
  gr.Markdown(SUBMISSION_PAGE_DESCRIPTION)
@@ -24,26 +32,18 @@ def get_submission_page(gr, demo):
24
 
25
  with gr.Row():
26
  with gr.Column():
27
- gr.LoginButton()
28
- username = gr.Textbox(
29
- label="Username from your Hugging Face account *",
30
- placeholder="Fetching from HuggingFace...",
31
- interactive=False,
32
- )
33
- name = gr.Textbox(
34
- label="Full name from your Hugging Face account *",
35
- placeholder="Fetching from HuggingFace...",
36
- interactive=False,
37
- )
38
- email = gr.Textbox(
39
- label="Email *", placeholder="myemail@domain.com", interactive=True
40
- )
41
  team_name = gr.Textbox(
42
  label="Team name *",
43
  placeholder="My Awesome Lab",
44
  info="Enter your team name.",
45
  value="",
46
  )
 
 
 
 
 
 
47
  hypothesis_dropdown = gr.Dropdown(
48
  label="Hypothesis *",
49
  choices=PROBLEM_TYPES,
@@ -75,8 +75,8 @@ def get_submission_page(gr, demo):
75
  is_valid = gr.State(value=False)
76
 
77
  predictions_file = gr.File(
78
- label="Prediction File (.xlsx) *",
79
- file_types=[".xlsx"],
80
  file_count="single",
81
  )
82
  submission_count_display = gr.Markdown(
@@ -92,24 +92,22 @@ def get_submission_page(gr, demo):
92
 
93
  # -- Submission count display when team / hypothesis changes ---------
94
  def on_submission_team_or_hypothesis_change(
95
- submission_username: str, submission_team_name: str, hypothesis: str
96
  ):
97
- if submission_username is None or not submission_username.strip():
98
- return (
99
- " ❌You must be logged in with your HuggingFace account to register"
100
- )
101
  if (
102
  submission_team_name is None
 
 
103
  or not submission_team_name.strip()
104
  or not hypothesis
105
  ):
106
  return "❌ Enter your team name and select a hypothesis to see the number of results already submitted."
107
 
108
- is_user_registered = validate_user_team_registration(
109
- username=submission_username, team_name=submission_team_name
110
  )
111
  if not is_user_registered:
112
- return "❌ The username corresponding to the account you are logged in with is not registered as part of the given team. Please register on the registration panel."
113
 
114
  count = get_team_submission_count(submission_team_name, hypothesis)
115
  remaining = MAX_SUBMISSIONS_PER_HYPOTHESIS - count
@@ -127,14 +125,14 @@ def get_submission_page(gr, demo):
127
  for widget in [team_name, hypothesis_dropdown]:
128
  widget.change(
129
  fn=on_submission_team_or_hypothesis_change,
130
- inputs=[username, team_name, hypothesis_dropdown],
131
  outputs=[submission_count_display],
132
  )
133
 
134
  # -- Prediction file upload & validation -----------------------------
135
  def on_submit(
136
  submission_team_name: str,
137
- submission_username: str,
138
  submission_hypothesis: str,
139
  submission_file: str,
140
  submission_note: str,
@@ -142,9 +140,9 @@ def get_submission_page(gr, demo):
142
  ):
143
  # --- guard: required fields ------------------------------------
144
  success, status = on_validation(
145
- file=submission_file,
146
  submission_team_name=submission_team_name,
147
- submission_username=submission_username,
148
  submission_hypothesis=submission_hypothesis,
149
  )
150
  if not success:
@@ -155,7 +153,7 @@ def get_submission_page(gr, demo):
155
  team_name=submission_team_name,
156
  hypothesis=submission_hypothesis,
157
  file_path=submission_file,
158
- uploader_username=submission_username,
159
  note=submission_note,
160
  link_to_methods=report_link,
161
  )
@@ -182,7 +180,7 @@ def get_submission_page(gr, demo):
182
  fn=on_submit,
183
  inputs=[
184
  team_name,
185
- username,
186
  hypothesis_dropdown,
187
  predictions_file,
188
  note,
@@ -193,41 +191,29 @@ def get_submission_page(gr, demo):
193
 
194
  def on_validation(
195
  file: str | None,
196
- submission_username: str,
197
  submission_team_name: str,
198
  submission_hypothesis: str,
199
  ) -> tuple[bool, str]:
200
  """Validate the given file according to the challenge's requirements."""
201
  errors: list[str] = []
202
- if not submission_username.strip():
203
- return False, gr.update(
204
- value=f"❌ You must be logged in with your HuggingFace account to register.",
205
- visible=True,
206
- )
207
  if not submission_team_name.strip():
208
  errors.append("Team name is required.")
 
 
209
  if submission_hypothesis is None:
210
  errors.append("Please select a hypothesis..")
211
  if errors:
212
  status = "❌ " + " | ".join(errors)
213
  return False, gr.update(value=status, visible=True)
214
 
215
- user_validation = validate_user_registration(
216
- username=submission_username, team_name=submission_team_name
217
  )
218
- if user_validation == "not registered":
219
- errors.append(
220
- "The username corresponding to the account you are logged in with is not registered as part of the given team. Please register on the registration panel."
221
- )
222
- elif user_validation == "pending":
223
- errors.append(
224
- "Your registration is still pending. Please wait until your registration is validated or contact the challenge organizers.."
225
- )
226
- elif user_validation == "not validated":
227
  errors.append(
228
- "Your registration was not accepted by the organizers. You can't submit your submission. Contact your organizers if you think it was an error."
229
  )
230
-
231
  if file is None:
232
  errors.append("Please upload a CSV file.")
233
 
@@ -259,20 +245,17 @@ def get_submission_page(gr, demo):
259
  visible=True,
260
  )
261
 
262
- success, message = validate(path=file)
 
263
  if not success:
264
- return False, gr.update(value=message, visible=True)
265
 
266
- return True, gr.update(value=message, visible=True)
 
 
267
 
268
  validation_button.click(
269
  fn=on_validation,
270
- inputs=[predictions_file, username, team_name, hypothesis_dropdown],
271
  outputs=[is_valid, validation_status],
272
  )
273
-
274
- demo.load(
275
- fn=prefill_user_info,
276
- inputs=[],
277
- outputs=[username, name, email],
278
- )
 
3
  get_team_submission_count,
4
  submit_prediction,
5
  validate_user_registration,
 
6
  )
7
+ from components.utils import check_team_has_leader
 
8
  from components.submission.config import (
9
  MAX_SUBMISSIONS_PER_HYPOTHESIS,
10
  SUBMISSION_PAGE_DESCRIPTION,
 
12
  )
13
 
14
 
15
+ def validate_csv(uploaded_file: str) -> tuple[bool, list[str]]:
16
+ """
17
+ Run the submission validator routine on the uploaded CSV.
18
+ Returns (passed: bool, messages: list[str]).
19
+ """
20
+ # TODO: implement your real validation logic
21
+ print(uploaded_file)
22
+ return True, []
23
+
24
+
25
+ def get_submission_page(gr):
26
+ with gr.TabItem("✉️ Submit"):
27
 
28
  # ── Section header ──────────────────────────────────────────────────
29
  gr.Markdown(SUBMISSION_PAGE_DESCRIPTION)
 
32
 
33
  with gr.Row():
34
  with gr.Column():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  team_name = gr.Textbox(
36
  label="Team name *",
37
  placeholder="My Awesome Lab",
38
  info="Enter your team name.",
39
  value="",
40
  )
41
+ uploader_email = gr.Textbox(
42
+ label=" Email *",
43
+ info="Enter your email.",
44
+ placeholder="you@university.edu",
45
+ value="",
46
+ )
47
  hypothesis_dropdown = gr.Dropdown(
48
  label="Hypothesis *",
49
  choices=PROBLEM_TYPES,
 
75
  is_valid = gr.State(value=False)
76
 
77
  predictions_file = gr.File(
78
+ label="Prediction File (.csv) *",
79
+ file_types=[".csv"],
80
  file_count="single",
81
  )
82
  submission_count_display = gr.Markdown(
 
92
 
93
  # -- Submission count display when team / hypothesis changes ---------
94
  def on_submission_team_or_hypothesis_change(
95
+ submission_email: str, submission_team_name: str, hypothesis: str
96
  ):
 
 
 
 
97
  if (
98
  submission_team_name is None
99
+ or submission_email is None
100
+ or not submission_email.strip()
101
  or not submission_team_name.strip()
102
  or not hypothesis
103
  ):
104
  return "❌ Enter your team name and select a hypothesis to see the number of results already submitted."
105
 
106
+ is_user_registered = validate_user_registration(
107
+ email=submission_email, team_name=submission_team_name
108
  )
109
  if not is_user_registered:
110
+ return "❌ The given email is not registered as part of the given team. Please provide a registered email and team or register on the registration panel."
111
 
112
  count = get_team_submission_count(submission_team_name, hypothesis)
113
  remaining = MAX_SUBMISSIONS_PER_HYPOTHESIS - count
 
125
  for widget in [team_name, hypothesis_dropdown]:
126
  widget.change(
127
  fn=on_submission_team_or_hypothesis_change,
128
+ inputs=[uploader_email, team_name, hypothesis_dropdown],
129
  outputs=[submission_count_display],
130
  )
131
 
132
  # -- Prediction file upload & validation -----------------------------
133
  def on_submit(
134
  submission_team_name: str,
135
+ submission_email: str,
136
  submission_hypothesis: str,
137
  submission_file: str,
138
  submission_note: str,
 
140
  ):
141
  # --- guard: required fields ------------------------------------
142
  success, status = on_validation(
143
+ file=predictions_file,
144
  submission_team_name=submission_team_name,
145
+ submission_email=submission_email,
146
  submission_hypothesis=submission_hypothesis,
147
  )
148
  if not success:
 
153
  team_name=submission_team_name,
154
  hypothesis=submission_hypothesis,
155
  file_path=submission_file,
156
+ uploader_email=submission_email,
157
  note=submission_note,
158
  link_to_methods=report_link,
159
  )
 
180
  fn=on_submit,
181
  inputs=[
182
  team_name,
183
+ uploader_email,
184
  hypothesis_dropdown,
185
  predictions_file,
186
  note,
 
191
 
192
  def on_validation(
193
  file: str | None,
194
+ submission_email: str,
195
  submission_team_name: str,
196
  submission_hypothesis: str,
197
  ) -> tuple[bool, str]:
198
  """Validate the given file according to the challenge's requirements."""
199
  errors: list[str] = []
 
 
 
 
 
200
  if not submission_team_name.strip():
201
  errors.append("Team name is required.")
202
+ if not submission_email.strip():
203
+ errors.append("Your email is required for compliance notifications.")
204
  if submission_hypothesis is None:
205
  errors.append("Please select a hypothesis..")
206
  if errors:
207
  status = "❌ " + " | ".join(errors)
208
  return False, gr.update(value=status, visible=True)
209
 
210
+ is_user_registered = validate_user_registration(
211
+ email=submission_email, team_name=submission_team_name
212
  )
213
+ if not is_user_registered:
 
 
 
 
 
 
 
 
214
  errors.append(
215
+ "Please provide your email address and the team you're registered into and the hypothesis corresponding to your hypothesis."
216
  )
 
217
  if file is None:
218
  errors.append("Please upload a CSV file.")
219
 
 
245
  visible=True,
246
  )
247
 
248
+ # TODO: add challenge requirements
249
+ success, message = validate_csv(uploaded_file=file)
250
  if not success:
251
+ return False, gr.update(value=f"❌ {message}.", visible=True)
252
 
253
+ return True, gr.update(
254
+ value=f"✅ The file validation was successful.", visible=True
255
+ )
256
 
257
  validation_button.click(
258
  fn=on_validation,
259
+ inputs=[predictions_file, uploader_email, team_name, hypothesis_dropdown],
260
  outputs=[is_valid, validation_status],
261
  )
 
 
 
 
 
 
components/submission/submission_template.xlsx DELETED
Binary file (5.36 kB)
 
components/submission/utils.py CHANGED
@@ -5,7 +5,6 @@ import pandas as pd
5
  from huggingface_hub import HfApi
6
 
7
  from about import TOKEN, REGISTRATION_REPO
8
- from components.registration.config import PARTICIPANTS_FILE_NAME, PARTICIPANT_COLUMNS
9
  from components.registration.utils import get_user_teams
10
  from components.submission.config import SUBMISSION_COLUMNS, SUBMISSIONS_FILE_NAME
11
  from components.utils import load_data_from_dataset, push_data_to_dataset
@@ -17,7 +16,7 @@ def submit_prediction(
17
  team_name: str,
18
  hypothesis: str,
19
  file_path: str,
20
- uploader_username: str,
21
  note: str | None,
22
  link_to_methods: str | None,
23
  ) -> tuple[bool, str]:
@@ -47,7 +46,7 @@ def submit_prediction(
47
  {
48
  "submission_id": submission_id,
49
  "team_name": team_name,
50
- "submission_username": uploader_username,
51
  "created_at": datetime.now(timezone.utc).isoformat(),
52
  "hypothesis": hypothesis,
53
  "submission_note": note or "",
@@ -80,40 +79,13 @@ def get_team_submission_count(team_name: str, hypothesis: str) -> int:
80
  ].shape[0]
81
 
82
 
83
- def validate_user_team_registration(username: str, team_name: str) -> bool:
84
  """
85
- Verify that the given username is registered as part of the team with the given team_name.
86
 
87
- :param username: The username to validate.
88
  :param team_name: The team to validate.
89
- :return: True is the username is registered as part of the team with the given team_name. False otherwise.
90
  """
91
- user_teams = get_user_teams(username=username)
92
  return len(user_teams[user_teams["team_name"] == team_name]) > 0
93
-
94
-
95
- def validate_user_registration(username: str, team_name: str) -> str:
96
- """
97
- Verify that the given username is validated as part of the team with the given team_name.
98
-
99
- :param username: The username to validate.
100
- :param team_name: The team to validate.
101
- :return: True is the username is registered and validated as part of the team with the given team_name. False otherwise.
102
- """
103
- participants_df = load_data_from_dataset(
104
- REGISTRATION_REPO, PARTICIPANTS_FILE_NAME, PARTICIPANT_COLUMNS
105
- )
106
- if participants_df.empty:
107
- return "not registered"
108
- record = participants_df[
109
- (participants_df["username"] == username)
110
- & (participants_df["team_name"] == team_name)
111
- ]
112
- if record.empty:
113
- return "not registered"
114
- elif record.iloc[0]["registration_status"] == "Validated":
115
- return "validated"
116
- elif record.iloc[0]["registration_status"] == "Pending":
117
- return "pending"
118
- else:
119
- return "not validated"
 
5
  from huggingface_hub import HfApi
6
 
7
  from about import TOKEN, REGISTRATION_REPO
 
8
  from components.registration.utils import get_user_teams
9
  from components.submission.config import SUBMISSION_COLUMNS, SUBMISSIONS_FILE_NAME
10
  from components.utils import load_data_from_dataset, push_data_to_dataset
 
16
  team_name: str,
17
  hypothesis: str,
18
  file_path: str,
19
+ uploader_email: str,
20
  note: str | None,
21
  link_to_methods: str | None,
22
  ) -> tuple[bool, str]:
 
46
  {
47
  "submission_id": submission_id,
48
  "team_name": team_name,
49
+ "submission_email": uploader_email,
50
  "created_at": datetime.now(timezone.utc).isoformat(),
51
  "hypothesis": hypothesis,
52
  "submission_note": note or "",
 
79
  ].shape[0]
80
 
81
 
82
+ def validate_user_registration(email: str, team_name: str) -> bool:
83
  """
84
+ Verify that the given email is registered as part of the team with the given team_name.
85
 
86
+ :param email: The email to validate.
87
  :param team_name: The team to validate.
88
+ :return: True is the email is registered as part of the team with the given team_name. False otherwise.
89
  """
90
+ user_teams = get_user_teams(email=email)
91
  return len(user_teams[user_teams["team_name"] == team_name]) > 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/submission/validate_submission.py DELETED
@@ -1,547 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -----------------------------------------------------------------------------
3
- # Papers & Findings submission validator — MecCog / APOE4 challenge
4
- # Author: Abigail Djossou
5
- # Date: 2026-07-03
6
- # -----------------------------------------------------------------------------
7
- """
8
- Validator for MecCog "papers and findings" submission spreadsheets.
9
-
10
- Based on the official spreadsheet description. Checks structure and per-column rules, and prints
11
- a clear report telling the submitter exactly what to fix if the sheet is not valid.
12
-
13
- Usage:
14
- python validate_submission.py submission.xlsx
15
- python validate_submission.py submission.xlsx --strict # warnings -> errors
16
-
17
- The spreadsheet layout (official):
18
- Row 1 : column labels (must all be present, correct columns)
19
- Row 2 : hypothesis being considered (must be present)
20
- Row 3+ : blocks, one per paper/source. Each block is:
21
- - a PAPER row : DOI, source type, PubMed ID, ID = P1, P2, ...
22
- - one FINDING row per finding : ID = P1.F1, P1.F2, ...
23
-
24
- Official columns:
25
- B DOI | C Paper/source type | D PubMed ID | E Paper/Finding ID |
26
- F Finding description | G Finding quote | H Finding summary |
27
- I Finding relevance (0..1) | J Experimental system | K Data location |
28
- L Effect size (int%) | M P value (<1.0) | N Sample size (int>0)
29
-
30
- Messages are written for challenge participants: they say which cell is wrong
31
- and what the expected format is, without referring to internal templates.
32
- """
33
-
34
- import re
35
- from openpyxl import load_workbook
36
- from openpyxl.utils import get_column_letter, column_index_from_string
37
-
38
- # ---- official spec ------------------------------------------------------
39
-
40
- ALLOWED_SOURCE_TYPES = {
41
- "pubmed published",
42
- "pubmed preprint",
43
- "web article",
44
- "database",
45
- "other",
46
- }
47
-
48
- # canonical field -> list of header aliases (lowercased, stripped) we accept
49
- FIELD_ALIASES = {
50
- "doi": ["doi"],
51
- "source_type": [
52
- "paper/source type",
53
- "paper type",
54
- "source type",
55
- "paper/source",
56
- "type",
57
- ],
58
- "pmid": ["pubmed id", "pmid", "pubmed"],
59
- "id": ["paper or source id", "paper/finding id", "finding id", "id", "code"],
60
- "finding_desc": ["finding description", "findings", "finding"],
61
- "quote": ["finding quote", "quote"],
62
- "summary": ["finding summary", "summary"],
63
- "relevance": ["finding relevance", "relevance"],
64
- "exp_system": ["experimental system", "exp system"],
65
- "data_location": [
66
- "data location",
67
- "data source",
68
- "data location in the source",
69
- "location",
70
- ],
71
- "effect_size": ["effect size"],
72
- "p_value": ["p value", "p-value", "pvalue"],
73
- "sample_size": ["sample size", "n"],
74
- }
75
-
76
- # official column letters (fallback if header mapping fails)
77
- OFFICIAL_COLS = {
78
- "doi": "B",
79
- "source_type": "C",
80
- "pmid": "D",
81
- "id": "E",
82
- "finding_desc": "F",
83
- "quote": "G",
84
- "summary": "H",
85
- "relevance": "I",
86
- "exp_system": "J",
87
- "data_location": "K",
88
- "effect_size": "L",
89
- "p_value": "M",
90
- "sample_size": "N",
91
- }
92
-
93
- # fields required on a FINDING row
94
- FINDING_REQUIRED = ["finding_desc", "relevance", "exp_system", "data_location"]
95
-
96
-
97
- class Report:
98
- def __init__(self):
99
- self.errors = []
100
- self.warnings = []
101
-
102
- def err(self, row, col, msg):
103
- self.errors.append((row, col, msg))
104
-
105
- def warn(self, row, col, msg):
106
- self.warnings.append((row, col, msg))
107
-
108
- def dump(self, strict=False) -> tuple[bool, str]:
109
- message = ""
110
- errs = list(self.errors)
111
- warns = list(self.warnings)
112
- if strict:
113
- errs += warns
114
- warns = []
115
- message += f"{'='*66}\n"
116
- if not errs and not warns:
117
- message += "✅ SUBMISSION VALID — no problems found.\n"
118
- message += "=" * 66
119
- message += "\nYour spreadsheet passed all checks. You can submit it.\n"
120
- return True, message
121
- status = "NOT VALID" if errs else "VALID (with suggestions)"
122
- message += "❌ " if errs else "✅ "
123
- message += f"SUBMISSION {status}: {len(errs)} thing(s) to fix, {len(warns)} suggestion(s)\n"
124
-
125
- message += "=" * 66
126
- if errs:
127
- message += "\n\nMUST FIX (the submission will be rejected until these are corrected):"
128
-
129
- for row, col, msg in sorted(errs, key=lambda x: (x[0] or 0, x[1] or "")):
130
- loc = f"row {row}" + (f", column {col}" if col else "")
131
- message += f"\n • [{loc}] {msg}"
132
- if warns:
133
- message += "\n\nPLEASE CHECK (allowed, but worth reviewing):"
134
- for row, col, msg in sorted(warns, key=lambda x: (x[0] or 0, x[1] or "")):
135
- loc = f"row {row}" + (f", column {col}" if col else "")
136
- message += f"\n • [{loc}] {msg}"
137
-
138
- return len(errs) == 0, message
139
-
140
-
141
- def norm(s):
142
- return re.sub(r"\s+", " ", str(s).strip().lower()) if s is not None else ""
143
-
144
-
145
- def map_columns(ws, rpt):
146
- """Map canonical field -> column letter, by header name, falling back to
147
- official letters. Returns (colmap, header_ok)."""
148
- header_row = 1
149
- headers = {}
150
- headers_raw = {}
151
- for c in range(1, ws.max_column + 1):
152
- v = ws.cell(row=header_row, column=c).value
153
- if v is not None and str(v).strip():
154
- letter = get_column_letter(c)
155
- headers[letter] = norm(v)
156
- headers_raw[letter] = str(v).strip()
157
-
158
- colmap = {}
159
- matched_by_header = {}
160
- used_headers = False
161
- matched_letters = set()
162
- for field, aliases in FIELD_ALIASES.items():
163
- found = None
164
- for letter, htext in headers.items():
165
- if htext in aliases:
166
- found = letter
167
- break
168
- if found:
169
- colmap[field] = found
170
- matched_by_header[field] = True
171
- matched_letters.add(found)
172
- used_headers = True
173
- else:
174
- colmap[field] = OFFICIAL_COLS.get(field)
175
- matched_by_header[field] = False
176
-
177
- # ---- header conformance check (reported up front) ----
178
- header_ok = True
179
- OFFICIAL_LABELS = {
180
- "doi": "DOI",
181
- "source_type": "Paper/source type",
182
- "pmid": "PubMed ID",
183
- "id": "Paper/Finding ID",
184
- "finding_desc": "Finding description",
185
- "quote": "Finding quote",
186
- "summary": "Finding summary",
187
- "relevance": "Finding relevance",
188
- "exp_system": "Experimental system",
189
- "data_location": "Data location",
190
- "effect_size": "Effect size",
191
- "p_value": "P value",
192
- "sample_size": "Sample size",
193
- }
194
- if not used_headers:
195
- rpt.err(
196
- 1,
197
- None,
198
- "The column headers in row 1 were not recognised. "
199
- "Please use the official submission template so the "
200
- "columns are: " + ", ".join(OFFICIAL_LABELS.values()) + ".",
201
- )
202
- header_ok = False
203
- else:
204
- missing = [
205
- OFFICIAL_LABELS[f]
206
- for f in (
207
- "doi",
208
- "source_type",
209
- "pmid",
210
- "id",
211
- "finding_desc",
212
- "quote",
213
- "summary",
214
- "relevance",
215
- "exp_system",
216
- "data_location",
217
- "effect_size",
218
- "p_value",
219
- "sample_size",
220
- )
221
- if not matched_by_header.get(f)
222
- ]
223
- if missing:
224
- header_ok = False
225
- rpt.err(
226
- 1,
227
- None,
228
- "These required columns are missing from row 1: "
229
- + ", ".join(missing)
230
- + ". Please add them (use the official "
231
- "submission template) so every column is present with its "
232
- "exact heading.",
233
- )
234
-
235
- # extra columns that aren't part of the official layout -> warn, ignored
236
- extra = []
237
- for letter, htext in headers.items():
238
- if letter == "A":
239
- continue # column A carries the hypothesis (row 2), no data header
240
- if letter not in matched_letters and htext:
241
- extra.append((letter, htext))
242
- for letter, htext in sorted(extra):
243
- shown = headers_raw.get(letter, htext)
244
- rpt.warn(
245
- 1,
246
- letter,
247
- f"Column '{shown}' is not an official column "
248
- f"and will be ignored. If this was meant to be "
249
- f"one of the required columns, please rename it "
250
- f"to the exact official heading.",
251
- )
252
- return colmap, header_ok
253
-
254
-
255
- def cell(ws, row, letter):
256
- if not letter:
257
- return None
258
- return ws.cell(row=row, column=column_index_from_string(letter)).value
259
-
260
-
261
- def is_paper_id(v):
262
- return bool(re.fullmatch(r"P\d+", str(v).strip())) if v is not None else False
263
-
264
-
265
- def is_finding_id(v):
266
- return bool(re.fullmatch(r"P\d+\.F\d+", str(v).strip())) if v is not None else False
267
-
268
-
269
- def validate(path, strict=False):
270
- rpt = Report()
271
- wb = load_workbook(path, data_only=True)
272
- ws = wb.active
273
- cm, header_ok = map_columns(ws, rpt)
274
-
275
- # ---- row 2: hypothesis present ----
276
- hyp = None
277
- for letter in ("A", cm.get("doi")):
278
- pass
279
- # hypothesis is expected in row 2, column A (or first non-empty cell)
280
- hyp_val = ws.cell(row=2, column=1).value
281
- if not hyp_val or not str(hyp_val).strip():
282
- # try any cell in row 2
283
- row2 = [ws.cell(row=2, column=c).value for c in range(1, ws.max_column + 1)]
284
- if not any(v and str(v).strip() for v in row2):
285
- rpt.err(2, "A", "Hypothesis being considered is missing (row 2).")
286
-
287
- # ---- iterate blocks from row 3 ----
288
- expected_paper_n = 1
289
- current_paper = None # e.g. "P1"
290
- finding_counters = {} # paper -> next finding index expected
291
- seen_dois = {}
292
-
293
- r = 3
294
- max_r = ws.max_row
295
- while r <= max_r:
296
- idv = cell(ws, r, cm["id"])
297
- doi = cell(ws, r, cm["doi"])
298
- # skip fully empty rows
299
- rowvals = [ws.cell(row=r, column=c).value for c in range(1, ws.max_column + 1)]
300
- if not any(v is not None and str(v).strip() for v in rowvals):
301
- r += 1
302
- continue
303
-
304
- if is_paper_id(idv):
305
- # ---- PAPER row ----
306
- pid = str(idv).strip()
307
- num = int(pid[1:])
308
- if num != expected_paper_n:
309
- rpt.err(
310
- r,
311
- cm["id"],
312
- f"Paper ID '{pid}' out of sequence; "
313
- f"expected 'P{expected_paper_n}'.",
314
- )
315
- expected_paper_n = num + 1
316
- current_paper = pid
317
- finding_counters[pid] = 1
318
-
319
- # DOI required
320
- if not doi or not str(doi).strip():
321
- rpt.err(r, cm["doi"], f"{pid}: DOI is missing.")
322
- else:
323
- d = str(doi).strip().lower()
324
- if not re.match(r"10\.\d{4,9}/\S+", d):
325
- rpt.warn(
326
- r,
327
- cm["doi"],
328
- f"{pid}: DOI '{doi}' does not look "
329
- f"like a standard DOI (10.xxxx/...).",
330
- )
331
- if d in seen_dois:
332
- rpt.err(
333
- r,
334
- cm["doi"],
335
- f"{pid}: this DOI is already used by "
336
- f"{seen_dois[d]}. Each source must have "
337
- f"a unique DOI.",
338
- )
339
- else:
340
- seen_dois[d] = pid
341
-
342
- # source type
343
- st = cell(ws, r, cm["source_type"])
344
- if not st or not str(st).strip():
345
- rpt.err(r, cm["source_type"], f"{pid}: Paper/source type missing.")
346
- elif norm(st) not in ALLOWED_SOURCE_TYPES:
347
- rpt.err(
348
- r,
349
- cm["source_type"],
350
- f"{pid}: source type '{st}' is not allowed. Use one of: "
351
- f"PubMed published, PubMed preprint, Web article, "
352
- f"Database, Other.",
353
- )
354
-
355
- # PMID (required for PubMed sources; recommended generally)
356
- pmid = cell(ws, r, cm["pmid"])
357
- st_norm = norm(st)
358
- if st_norm.startswith("pubmed"):
359
- if not pmid or not str(pmid).strip():
360
- rpt.err(
361
- r,
362
- cm["pmid"],
363
- f"{pid}: PubMed ID required for " f"PubMed sources.",
364
- )
365
- elif not re.fullmatch(r"\d+", str(pmid).strip()):
366
- rpt.err(
367
- r,
368
- cm["pmid"],
369
- f"{pid}: PubMed ID '{pmid}' must be " f"digits only.",
370
- )
371
-
372
- elif is_finding_id(idv):
373
- # ---- FINDING row ----
374
- fid = str(idv).strip()
375
- fpaper, findex = fid.split(".")
376
- if current_paper is None:
377
- rpt.err(r, cm["id"], f"Finding '{fid}' appears before any paper row.")
378
- elif fpaper != current_paper:
379
- rpt.err(
380
- r,
381
- cm["id"],
382
- f"Finding '{fid}' does not belong to current "
383
- f"paper '{current_paper}'.",
384
- )
385
- else:
386
- exp = finding_counters.get(current_paper, 1)
387
- if int(findex[1:]) != exp:
388
- rpt.err(
389
- r,
390
- cm["id"],
391
- f"Finding '{fid}' out of sequence; "
392
- f"expected '{current_paper}.F{exp}'.",
393
- )
394
- finding_counters[current_paper] = int(findex[1:]) + 1
395
-
396
- # required finding fields (must be non-empty)
397
- READABLE = {
398
- "finding_desc": "Finding description",
399
- "relevance": "Finding relevance",
400
- "exp_system": "Experimental system",
401
- "data_location": "Data location",
402
- }
403
- for fld in FINDING_REQUIRED:
404
- v = cell(ws, r, cm[fld])
405
- if v is None or not str(v).strip():
406
- rpt.err(
407
- r,
408
- cm[fld],
409
- f"{fid}: '{READABLE[fld]}' is required "
410
- f"but empty. Please fill it in.",
411
- )
412
-
413
- # quote and summary must contain a value OR the text 'N/A' (never blank)
414
- for fld, label in (
415
- ("quote", "Finding quote"),
416
- ("summary", "Finding summary"),
417
- ):
418
- v = cell(ws, r, cm[fld])
419
- if v is None or not str(v).strip():
420
- rpt.err(
421
- r,
422
- cm[fld],
423
- f"{fid}: '{label}' must not be empty. "
424
- f"Enter the {label.lower()}, or 'N/A' if there is none.",
425
- )
426
-
427
- # relevance 0..1
428
- rel = cell(ws, r, cm["relevance"])
429
- if rel is not None and str(rel).strip():
430
- try:
431
- rv = float(rel)
432
- if not (0.0 <= rv <= 1.0):
433
- rpt.err(
434
- r,
435
- cm["relevance"],
436
- f"{fid}: relevance must be a "
437
- f"number between 0 and 1 (found {rv}).",
438
- )
439
- except ValueError:
440
- rpt.err(
441
- r,
442
- cm["relevance"],
443
- f"{fid}: relevance must be a "
444
- f"number between 0 and 1 (found '{rel}').",
445
- )
446
-
447
- # data location precision (warn if whole-figure only)
448
- loc = cell(ws, r, cm["data_location"])
449
- if loc and str(loc).strip():
450
- locs = str(loc).strip()
451
- if re.fullmatch(r"(fig(ure)?\s*\d+)", locs, flags=re.I):
452
- rpt.warn(
453
- r,
454
- cm["data_location"],
455
- f"{fid}: location '{locs}' may be imprecise "
456
- f"(panel not specified, e.g. Fig4C).",
457
- )
458
-
459
- # effect size: whole number followed by %, or N/A (must not be blank)
460
- es = cell(ws, r, cm.get("effect_size"))
461
- if es is None or not str(es).strip():
462
- rpt.err(
463
- r,
464
- cm.get("effect_size"),
465
- f"{fid}: 'Effect size' must "
466
- f"not be empty. Enter a whole number + '%' (e.g. 300%), "
467
- f"or 'N/A' if not available/applicable.",
468
- )
469
- elif norm(es) != "n/a" and not re.fullmatch(r"\d+%", str(es).strip()):
470
- rpt.err(
471
- r,
472
- cm.get("effect_size"),
473
- f"{fid}: effect size '{es}' must be a whole number "
474
- f"followed by '%' (e.g. 300%), or 'N/A'.",
475
- )
476
-
477
- # p value: number < 1.0, or N/A (must not be blank)
478
- pv = cell(ws, r, cm.get("p_value"))
479
- if pv is None or not str(pv).strip():
480
- rpt.err(
481
- r,
482
- cm.get("p_value"),
483
- f"{fid}: 'P value' must not be "
484
- f"empty. Enter a number less than 1.0, or 'N/A' if not "
485
- f"available/applicable.",
486
- )
487
- elif norm(pv) != "n/a":
488
- m = re.search(r"[-+]?\d*\.?\d+(e[-+]?\d+)?", str(pv), flags=re.I)
489
- if not m:
490
- rpt.err(
491
- r,
492
- cm.get("p_value"),
493
- f"{fid}: p value '{pv}' must be a number less than "
494
- f"1.0, or 'N/A'.",
495
- )
496
- else:
497
- try:
498
- if float(m.group()) >= 1.0:
499
- rpt.err(
500
- r,
501
- cm.get("p_value"),
502
- f"{fid}: p value '{pv}' must be less than 1.0.",
503
- )
504
- except ValueError:
505
- rpt.err(
506
- r,
507
- cm.get("p_value"),
508
- f"{fid}: p value '{pv}' must be a number less "
509
- f"than 1.0, or 'N/A'.",
510
- )
511
-
512
- # sample size: whole number > 0, or N/A (must not be blank)
513
- ss = cell(ws, r, cm.get("sample_size"))
514
- if ss is None or not str(ss).strip():
515
- rpt.err(
516
- r,
517
- cm.get("sample_size"),
518
- f"{fid}: 'Sample size' must "
519
- f"not be empty. Enter a whole number greater than 0, or "
520
- f"'N/A' if not applicable.",
521
- )
522
- elif norm(ss) != "n/a":
523
- if not re.fullmatch(r"\d+", str(ss).strip()) or int(ss) <= 0:
524
- rpt.err(
525
- r,
526
- cm.get("sample_size"),
527
- f"{fid}: sample size '{ss}' must be a whole number "
528
- f"greater than 0, or 'N/A'.",
529
- )
530
-
531
- else:
532
- # a non-empty row whose ID cell isn't a valid P#/P#.F# code
533
- if idv is not None and str(idv).strip():
534
- rpt.err(
535
- r,
536
- cm["id"],
537
- f"ID '{idv}' is neither a paper ID (P1) "
538
- f"nor a finding ID (P1.F1).",
539
- )
540
- else:
541
- rpt.warn(r, cm["id"], "Non-empty row with no ID in the ID column.")
542
- r += 1
543
-
544
- if expected_paper_n == 1:
545
- rpt.err(None, None, "No paper/source blocks found (expected P1, P2, ...).")
546
-
547
- return rpt.dump(strict=strict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/utils.py CHANGED
@@ -4,10 +4,7 @@ import pathlib
4
  import tempfile
5
  import json
6
  from pathlib import Path
7
- import httpx
8
 
9
-
10
- import gradio
11
  import gradio as gr
12
  import pandas as pd
13
  from huggingface_hub import hf_hub_download, HfApi
@@ -166,30 +163,4 @@ def check_team_has_leader(team_name: str) -> bool:
166
  team = get_team(teams_df, team_name)
167
  if team is None:
168
  return False
169
- return bool((team.get("leader_username") or "").strip())
170
-
171
-
172
- def prefill_user_info(
173
- oauth_profile: gradio.OAuthProfile | None,
174
- oauth_token: gradio.OAuthToken | None,
175
- ):
176
- empty = lambda: gr.update(value="", placeholder="Log in with HuggingFace first")
177
- if oauth_profile is None or oauth_token is None:
178
- return empty(), empty(), empty()
179
-
180
- try:
181
- resp = httpx.get(
182
- "https://huggingface.co/oauth/userinfo",
183
- headers={"Authorization": f"Bearer {oauth_token.token}"},
184
- timeout=5,
185
- )
186
- resp.raise_for_status()
187
- email = resp.json().get("email", "")
188
- except Exception:
189
- email = ''
190
-
191
- return (
192
- gr.update(value=oauth_profile.username),
193
- gr.update(value=oauth_profile.name),
194
- gr.update(value=email),
195
- )
 
4
  import tempfile
5
  import json
6
  from pathlib import Path
 
7
 
 
 
8
  import gradio as gr
9
  import pandas as pd
10
  from huggingface_hub import hf_hub_download, HfApi
 
163
  team = get_team(teams_df, team_name)
164
  if team is None:
165
  return False
166
+ return bool((team.get("leader_email") or "").strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages.txt DELETED
@@ -1,3 +0,0 @@
1
- build-essential
2
- cmake
3
- libnetcdf-dev
 
 
 
 
requirements.txt CHANGED
@@ -1,10 +1,7 @@
1
- gradio==5.50.0
2
  datasets
3
  huggingface_hub
4
- httpx
5
  gradio-leaderboard
6
  plotly
7
  dotenv
8
- pandas
9
- gradio[oauth]
10
- openpyxl
 
1
+ gradio
2
  datasets
3
  huggingface_hub
 
4
  gradio-leaderboard
5
  plotly
6
  dotenv
7
+ pandas