DineshAI commited on
Commit
46daaad
·
verified ·
1 Parent(s): 5aa0e72

Add opt-in public certificate links

Browse files

Publish certificates only when users opt in, expose a permanent Hub URL, and pass it to LinkedIn as certUrl.\n\nRequires an HF_TOKEN Space secret with write access to ICML-2026-agent-repro/certificates.

.pytest_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Created by pytest automatically.
2
+ *
.pytest_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
.pytest_cache/v/cache/lastfailed ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
.pytest_cache/v/cache/nodeids ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "test_app.py::test_auth_control_uses_sandbox_safe_navigation",
3
+ "test_app.py::test_certificate_is_not_published_without_opt_in",
4
+ "test_app.py::test_certificate_is_published_after_opt_in",
5
+ "test_app.py::test_linkedin_url_includes_public_certificate_url",
6
+ "test_app.py::test_oauth_login_clears_stale_session_before_redirecting",
7
+ "test_app.py::test_publish_certificate_uses_stable_public_path",
8
+ "test_app.py::test_root_frontend_asset_is_served"
9
+ ]
README.md CHANGED
@@ -18,3 +18,8 @@ Eligibility: at least one claim judged **verified** by the automated Logbook
18
  Judge before the challenge deadline (frozen in `eligible.json` at challenge
19
  close; 267 participants qualify). Certificates render in-Space with headless
20
  Chromium and include your verified-claim statistics and partner logos.
 
 
 
 
 
 
18
  Judge before the challenge deadline (frozen in `eligible.json` at challenge
19
  close; 267 participants qualify). Certificates render in-Space with headless
20
  Chromium and include your verified-claim statistics and partner logos.
21
+
22
+ Participants can optionally publish their generated certificate to the public
23
+ `ICML-2026-agent-repro/certificates` dataset. The resulting permanent URL is
24
+ shown in the app and used as LinkedIn's credential URL. The Space needs an
25
+ `HF_TOKEN` secret with write access to that dataset.
app.py CHANGED
@@ -20,8 +20,13 @@ import gradio as gr
20
  import uvicorn
21
  from fastapi import FastAPI
22
  from fastapi.responses import RedirectResponse
 
23
 
24
  DISCUSSIONS_URL = "https://huggingface.co/spaces/ICML-2026-agent-repro/challenge/discussions"
 
 
 
 
25
  CANVAS = (2000, 1414) # must match the body size in certificate_template.html
26
 
27
  _HERE = os.path.dirname(__file__)
@@ -110,18 +115,35 @@ def on_load(profile: gr.OAuthProfile | None):
110
  profile.name or profile.username)
111
 
112
 
113
- def linkedin_url(name):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  p = {
115
  "startTask": "CERTIFICATION_NAME",
116
  "name": "ICML 2026 Open Reproductions — Certificate of Participation",
117
  "organizationName": "Hugging Face",
118
  "issueYear": "2026", "issueMonth": "8",
119
- "certUrl": "https://huggingface.co/spaces/ICML-2026-agent-repro/certificate-generator",
 
120
  }
121
  return "https://www.linkedin.com/profile/add?" + urllib.parse.urlencode(p)
122
 
123
 
124
- def create_certificate(display_name, profile: gr.OAuthProfile | None):
125
  if profile is None:
126
  return None, "❌ Please sign in first.", gr.update(visible=False)
127
  rec = ELIGIBLE.get(profile.username.lower())
@@ -133,10 +155,26 @@ def create_certificate(display_name, profile: gr.OAuthProfile | None):
133
  image_path = render_html(html)
134
  except Exception as e:
135
  return None, f"❌ Rendering failed: {e}. Please try again in a minute.", gr.update(visible=False)
136
- btn = (f'<a href="{linkedin_url(name)}" target="_blank" '
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  f'style="display:inline-block;background:#0a66c2;color:#fff;font-weight:700;'
138
  f'padding:10px 22px;border-radius:8px;text-decoration:none">Add to LinkedIn profile →</a>')
139
- return image_path, "🎉 Your certificate is ready — download it or add it to your LinkedIn profile below.", gr.update(value=btn, visible=True)
140
 
141
 
142
  THEME = gr.themes.Base(
@@ -175,13 +213,24 @@ with gr.Blocks(title="ICML 2026 Open Reproductions — Certificates", theme=THEM
175
  status = gr.Markdown()
176
  with gr.Column(visible=False) as main_box:
177
  name_box = gr.Textbox(label="Name on the certificate")
 
 
 
 
 
 
 
178
  gen_btn = gr.Button("Generate my certificate 🎓", variant="primary")
179
  cert_image = gr.Image(label="Your certificate", type="filepath", interactive=False)
180
  note = gr.Markdown()
181
  linkedin = gr.HTML(visible=False)
182
 
183
  demo.load(on_load, inputs=None, outputs=[auth, main_box, status, name_box])
184
- gen_btn.click(create_certificate, inputs=[name_box], outputs=[cert_image, note, linkedin])
 
 
 
 
185
 
186
 
187
  app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
 
20
  import uvicorn
21
  from fastapi import FastAPI
22
  from fastapi.responses import RedirectResponse
23
+ from huggingface_hub import upload_file
24
 
25
  DISCUSSIONS_URL = "https://huggingface.co/spaces/ICML-2026-agent-repro/challenge/discussions"
26
+ SPACE_URL = "https://huggingface.co/spaces/ICML-2026-agent-repro/certificate-generator"
27
+ CERTIFICATES_DATASET = os.getenv(
28
+ "CERTIFICATES_DATASET", "ICML-2026-agent-repro/certificates"
29
+ )
30
  CANVAS = (2000, 1414) # must match the body size in certificate_template.html
31
 
32
  _HERE = os.path.dirname(__file__)
 
115
  profile.name or profile.username)
116
 
117
 
118
+ def publish_certificate(username, image_path):
119
+ path_in_repo = f"certificates/{username.lower()}.png"
120
+ upload_file(
121
+ path_or_fileobj=image_path,
122
+ path_in_repo=path_in_repo,
123
+ repo_id=CERTIFICATES_DATASET,
124
+ repo_type="dataset",
125
+ token=os.getenv("HF_TOKEN"),
126
+ )
127
+ encoded_path = urllib.parse.quote(path_in_repo, safe="/")
128
+ return (
129
+ f"https://huggingface.co/datasets/{CERTIFICATES_DATASET}/"
130
+ f"resolve/main/{encoded_path}"
131
+ )
132
+
133
+
134
+ def linkedin_url(cert_url, username):
135
  p = {
136
  "startTask": "CERTIFICATION_NAME",
137
  "name": "ICML 2026 Open Reproductions — Certificate of Participation",
138
  "organizationName": "Hugging Face",
139
  "issueYear": "2026", "issueMonth": "8",
140
+ "certUrl": cert_url,
141
+ "certId": username,
142
  }
143
  return "https://www.linkedin.com/profile/add?" + urllib.parse.urlencode(p)
144
 
145
 
146
+ def create_certificate(display_name, make_public, profile: gr.OAuthProfile | None):
147
  if profile is None:
148
  return None, "❌ Please sign in first.", gr.update(visible=False)
149
  rec = ELIGIBLE.get(profile.username.lower())
 
155
  image_path = render_html(html)
156
  except Exception as e:
157
  return None, f"❌ Rendering failed: {e}. Please try again in a minute.", gr.update(visible=False)
158
+
159
+ cert_url = SPACE_URL
160
+ if make_public:
161
+ try:
162
+ cert_url = publish_certificate(profile.username, image_path)
163
+ except Exception as e:
164
+ print(f"Certificate upload failed for @{profile.username}: {e}")
165
+ note = ("⚠️ Your certificate is ready to download, but its public URL could not "
166
+ "be created. Please try generating it again in a minute.")
167
+ else:
168
+ note = ("🎉 Your certificate is ready. Anyone with your "
169
+ f"[public certificate URL]({cert_url}) can view it.")
170
+ else:
171
+ note = ("🎉 Your certificate is ready to download. It was not published; enable "
172
+ "the public sharing option and generate it again to create a permanent URL.")
173
+
174
+ btn = (f'<a href="{linkedin_url(cert_url, profile.username)}" target="_blank" '
175
  f'style="display:inline-block;background:#0a66c2;color:#fff;font-weight:700;'
176
  f'padding:10px 22px;border-radius:8px;text-decoration:none">Add to LinkedIn profile →</a>')
177
+ return image_path, note, gr.update(value=btn, visible=True)
178
 
179
 
180
  THEME = gr.themes.Base(
 
213
  status = gr.Markdown()
214
  with gr.Column(visible=False) as main_box:
215
  name_box = gr.Textbox(label="Name on the certificate")
216
+ public_share = gr.Checkbox(
217
+ value=False,
218
+ label="Create a public shareable certificate URL",
219
+ info=("Publishes your certificate image and displayed name in a public Hugging "
220
+ "Face dataset. Your username appears in the URL; generating again replaces "
221
+ "the image at that URL."),
222
+ )
223
  gen_btn = gr.Button("Generate my certificate 🎓", variant="primary")
224
  cert_image = gr.Image(label="Your certificate", type="filepath", interactive=False)
225
  note = gr.Markdown()
226
  linkedin = gr.HTML(visible=False)
227
 
228
  demo.load(on_load, inputs=None, outputs=[auth, main_box, status, name_box])
229
+ gen_btn.click(
230
+ create_certificate,
231
+ inputs=[name_box, public_share],
232
+ outputs=[cert_image, note, linkedin],
233
+ )
234
 
235
 
236
  app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
test_app.py CHANGED
@@ -2,6 +2,7 @@ import importlib
2
  import os
3
  import re
4
  from types import SimpleNamespace
 
5
 
6
  from fastapi.testclient import TestClient
7
 
@@ -59,3 +60,75 @@ def test_root_frontend_asset_is_served():
59
  asset_response = client.get(asset_path)
60
 
61
  assert asset_response.status_code == 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import os
3
  import re
4
  from types import SimpleNamespace
5
+ from urllib.parse import parse_qs, quote, urlparse
6
 
7
  from fastapi.testclient import TestClient
8
 
 
60
  asset_response = client.get(asset_path)
61
 
62
  assert asset_response.status_code == 200
63
+
64
+
65
+ def eligible_profile():
66
+ username = next(iter(certificate_app.ELIGIBLE))
67
+ return SimpleNamespace(username=username, name="Test Participant")
68
+
69
+
70
+ def test_linkedin_url_includes_public_certificate_url():
71
+ url = certificate_app.linkedin_url(
72
+ "https://huggingface.co/datasets/example/certificates/resolve/main/test.png",
73
+ "test-user",
74
+ )
75
+ params = parse_qs(urlparse(url).query)
76
+
77
+ assert params["certUrl"] == [
78
+ "https://huggingface.co/datasets/example/certificates/resolve/main/test.png"
79
+ ]
80
+ assert params["certId"] == ["test-user"]
81
+
82
+
83
+ def test_publish_certificate_uses_stable_public_path(monkeypatch):
84
+ upload = {}
85
+ monkeypatch.setenv("HF_TOKEN", "test-token")
86
+ monkeypatch.setattr(
87
+ certificate_app,
88
+ "upload_file",
89
+ lambda **kwargs: upload.update(kwargs),
90
+ )
91
+
92
+ url = certificate_app.publish_certificate("Test-User", "/tmp/certificate.png")
93
+
94
+ assert upload == {
95
+ "path_or_fileobj": "/tmp/certificate.png",
96
+ "path_in_repo": "certificates/test-user.png",
97
+ "repo_id": certificate_app.CERTIFICATES_DATASET,
98
+ "repo_type": "dataset",
99
+ "token": "test-token",
100
+ }
101
+ assert url.endswith("/resolve/main/certificates/test-user.png")
102
+
103
+
104
+ def test_certificate_is_not_published_without_opt_in(monkeypatch):
105
+ profile = eligible_profile()
106
+ monkeypatch.setattr(certificate_app, "render_html", lambda html: "/tmp/certificate.png")
107
+
108
+ def unexpected_upload(username, image_path):
109
+ raise AssertionError("certificate should not be published")
110
+
111
+ monkeypatch.setattr(certificate_app, "publish_certificate", unexpected_upload)
112
+
113
+ image_path, note, linkedin = certificate_app.create_certificate("Test", False, profile)
114
+
115
+ assert image_path == "/tmp/certificate.png"
116
+ assert "was not published" in note
117
+ assert quote(certificate_app.SPACE_URL, safe="") in linkedin["value"]
118
+
119
+
120
+ def test_certificate_is_published_after_opt_in(monkeypatch):
121
+ profile = eligible_profile()
122
+ public_url = "https://huggingface.co/datasets/example/certificates/resolve/main/test.png"
123
+ monkeypatch.setattr(certificate_app, "render_html", lambda html: "/tmp/certificate.png")
124
+ monkeypatch.setattr(
125
+ certificate_app,
126
+ "publish_certificate",
127
+ lambda username, image_path: public_url,
128
+ )
129
+
130
+ image_path, note, linkedin = certificate_app.create_certificate("Test", True, profile)
131
+
132
+ assert image_path == "/tmp/certificate.png"
133
+ assert public_url in note
134
+ assert quote(public_url, safe="") in linkedin["value"]