import os
from datetime import datetime
import random
import requests
from io import BytesIO
from datetime import date
import tempfile
from PIL import Image, ImageDraw, ImageFont
from huggingface_hub import upload_file
import pandas as pd
from huggingface_hub import HfApi, hf_hub_download, Repository
from huggingface_hub.repocard import metadata_load
import gradio as gr
from datasets import load_dataset, Dataset
from huggingface_hub import whoami
import asyncio
from functools import partial
EXAM_DATASET_ID = os.getenv("EXAM_DATASET_ID") or "agents-course/unit_1_quiz"
EXAM_MAX_QUESTIONS = os.getenv("EXAM_MAX_QUESTIONS") or 1
EXAM_PASSING_SCORE = os.getenv("EXAM_PASSING_SCORE") or 0.8
CERTIFYING_ORG_LINKEDIN_ID = os.getenv("CERTIFYING_ORG_LINKEDIN_ID", "000000")
COURSE_TITLE = os.getenv("COURSE_TITLE", "Fundamentals of MCP")
ds = load_dataset(EXAM_DATASET_ID, split="train")
DATASET_REPO_URL = "https://huggingface.co/datasets/mcp-course/certificates"
# Convert dataset to a list of dicts and randomly sort
quiz_data = ds.to_pandas().to_dict("records")
random.shuffle(quiz_data)
# Limit to max questions if specified
if EXAM_MAX_QUESTIONS:
quiz_data = quiz_data[: int(EXAM_MAX_QUESTIONS)]
def on_user_logged_in(token: gr.OAuthToken | None):
"""
If the user has a valid token, show Start button.
Otherwise, keep the login button visible.
"""
if token is not None:
return [
gr.update(visible=False), # login_btn
gr.update(visible=True), # start_btn
gr.update(visible=False), # next_btn
gr.update(visible=False), # submit_btn
"", # question_text
gr.update(choices=[], visible=False), # radio_choices
"Click 'Start' to begin the quiz", # status_text
0, # question_idx
[], # user_answers
gr.update(visible=False), # certificate_img
gr.update(
visible=True,
value="""
🎯 Complete the Quiz to Unlock
Pass the quiz to add your certificate to LinkedIn!
""",
), # linkedin_btn - now visible with explanatory text
token, # user_token
]
else:
return [
gr.update(visible=True), # login_btn
gr.update(visible=False), # start_btn
gr.update(visible=False), # next_btn
gr.update(visible=False), # submit_btn
"", # question_text
gr.update(choices=[], visible=False), # radio_choices
"", # status_text
0, # question_idx
[], # user_answers
gr.update(visible=False), # certificate_img
gr.update(
visible=True,
value="""
🔒 Login Required
""",
), # linkedin_btn - visible with login prompt
None, # user_token
]
def generate_certificate(name: str, profile_url: str):
"""Generate certificate image and PDF."""
certificate_path = os.path.join(
os.path.dirname(__file__), "templates", "certificate.png"
)
im = Image.open(certificate_path)
d = ImageDraw.Draw(im)
name_font = ImageFont.truetype("Quattrocento-Regular.ttf", 100)
date_font = ImageFont.truetype("Quattrocento-Regular.ttf", 48)
name = name.title()
d.text((1000, 740), name, fill="black", anchor="mm", font=name_font)
d.text((1480, 1170), str(date.today()), fill="black", anchor="mm", font=date_font)
pdf = im.convert("RGB")
pdf.save("certificate.pdf")
return im, "certificate.pdf"
def create_linkedin_button(username: str, cert_url: str | None) -> str:
"""Create LinkedIn 'Add to Profile' button HTML."""
current_year = date.today().year
current_month = date.today().month
# Use the dataset certificate URL if available, otherwise fallback to default
certificate_url = cert_url or "https://huggingface.co/mcp-course"
linkedin_params = {
"startTask": "CERTIFICATION_NAME",
"name": COURSE_TITLE,
"organizationName": "Hugging Face",
"organizationId": CERTIFYING_ORG_LINKEDIN_ID,
"organizationIdissueYear": str(current_year),
"issueMonth": str(current_month),
"certUrl": certificate_url,
"certId": username, # Using username as cert ID
}
# Build the LinkedIn button URL
base_url = "https://www.linkedin.com/profile/add?"
params = "&".join(
f"{k}={requests.utils.quote(v)}" for k, v in linkedin_params.items()
)
button_url = base_url + params
message = f"""
"""
return message
async def upload_certificate_to_hub(username: str, certificate_img) -> str:
"""Upload certificate to the dataset hub and return the URL asynchronously."""
# Save image to temporary file
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
certificate_img.save(tmp.name)
try:
# Run upload in a thread pool since upload_file is blocking
loop = asyncio.get_event_loop()
upload_func = partial(
upload_file,
path_or_fileobj=tmp.name,
path_in_repo=f"certificates/{username}/{date.today()}.png",
repo_id="mcp-course/certificates",
repo_type="dataset",
token=os.getenv("HF_TOKEN"),
)
await loop.run_in_executor(None, upload_func)
# Construct the URL to the image
cert_url = (
f"https://huggingface.co/datasets/mcp-course/certificates/"
f"resolve/main/certificates/{username}/{date.today()}.png"
)
# Clean up temp file
os.unlink(tmp.name)
return cert_url
except Exception as e:
print(f"Error uploading certificate: {e}")
os.unlink(tmp.name)
return None
async def push_results_to_hub(
user_answers,
custom_name: str | None,
token: gr.OAuthToken | None,
profile: gr.OAuthProfile | None,
):
"""Handle quiz completion and certificate generation."""
if token is None or profile is None:
gr.Warning("Please log in to Hugging Face before submitting!")
return (
gr.update(visible=True, value="Please login first"),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False), # hide custom name input
)
# Calculate grade
correct_count = sum(1 for answer in user_answers if answer["is_correct"])
total_questions = len(user_answers)
grade = correct_count / total_questions if total_questions > 0 else 0
if grade < float(EXAM_PASSING_SCORE):
return (
gr.update(visible=True, value=f"You scored {grade:.1%}..."),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False), # hide custom name input
)
try:
# Use custom name if provided, otherwise use profile name
name = (
custom_name.strip() if custom_name and custom_name.strip() else profile.name
)
# Generate certificate
certificate_img, _ = generate_certificate(
name=name, profile_url=profile.picture
)
# Start certificate upload asynchronously
gr.Info("Uploading your certificate...")
cert_url = await upload_certificate_to_hub(profile.username, certificate_img)
if cert_url is None:
gr.Warning("Certificate upload failed, but you still passed!")
cert_url = "https://huggingface.co/mcp-course"
# Create LinkedIn button
linkedin_button = create_linkedin_button(profile.username, cert_url)
result_message = f"""
🎉 Congratulations! You passed with a score of {grade:.1%}!
{linkedin_button}
"""
return (
gr.update(visible=True, value=result_message),
gr.update(visible=True, value=certificate_img),
gr.update(visible=True),
gr.update(visible=True), # show custom name input
)
except Exception as e:
print(f"Error generating certificate: {e}")
return (
gr.update(visible=True, value=f"🎉 You passed with {grade:.1%}!"),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False), # hide custom name input
)
def handle_quiz(
question_idx,
user_answers,
selected_answer,
is_start,
token: gr.OAuthToken | None,
profile: gr.OAuthProfile | None,
):
"""Handle quiz state transitions and store answers"""
if token is None or profile is None:
gr.Warning("Please log in to Hugging Face before starting the quiz!")
return [
"", # question_text
gr.update(choices=[], visible=False), # radio choices
"Please login first", # status_text
question_idx, # question_idx
user_answers, # user_answers
gr.update(visible=True), # start button
gr.update(visible=False), # next button
gr.update(visible=False), # submit button
gr.update(visible=False), # certificate image
gr.update(
visible=True,
value="""
🔒 Login Required
Please log in with your Hugging Face account to access the quiz and earn your LinkedIn certificate!
""",
), # linkedin button with login prompt
]
if not is_start and question_idx < len(quiz_data):
current_q = quiz_data[question_idx]
correct_reference = current_q["correct_answer"]
correct_reference = f"answer_{correct_reference}".lower()
is_correct = selected_answer == current_q[correct_reference]
user_answers.append(
{
"question": current_q["question"],
"selected_answer": selected_answer,
"correct_answer": current_q[correct_reference],
"is_correct": is_correct,
"correct_reference": correct_reference,
}
)
question_idx += 1
if question_idx >= len(quiz_data):
correct_count = sum(1 for answer in user_answers if answer["is_correct"])
grade = correct_count / len(user_answers)
has_passed = grade >= float(EXAM_PASSING_SCORE)
# LinkedIn button text for quiz completion
linkedin_completion_text = (
"""
🎉 Ready for LinkedIn!
Great! Click "Get your certificate" above to unlock the LinkedIn button.
"""
if has_passed
else """
❌ Try Again
You need a higher score to earn the LinkedIn certificate. Please retake the quiz!
"""
)
return [
"", # question_text
gr.update(choices=[], visible=False), # radio choices
f"{'🎉 Passed! Click now on 🎓 Get your certificate!' if has_passed else '❌ Did not pass'}", # status_text
question_idx, # question_idx
user_answers, # user_answers
gr.update(visible=False), # start button
gr.update(visible=False), # next button
gr.update(
visible=True,
value="🎓 Get your certificate" if has_passed else "❌ Did not pass",
interactive=has_passed,
), # submit button
gr.update(visible=False), # certificate image
gr.update(visible=True, value=linkedin_completion_text), # linkedin button
]
# Show next question
q = quiz_data[question_idx]
return [
f"## Question {question_idx + 1} \n### {q['question']}", # question_text
gr.update( # radio choices
choices=[q["answer_a"], q["answer_b"], q["answer_c"], q["answer_d"]],
value=None,
visible=True,
),
"Select an answer and click 'Next' to continue.", # status_text
question_idx, # question_idx
user_answers, # user_answers
gr.update(visible=False), # start button
gr.update(visible=True), # next button
gr.update(visible=False), # submit button
gr.update(visible=False), # certificate image
gr.update(
visible=True,
value="""
🎯 Keep Going!
Complete the quiz and pass to unlock your LinkedIn certificate!
""",
), # linkedin button with progress message
]
def success_message(response):
# response is whatever push_results_to_hub returned
return f"{response}\n\n**Success!**"
with gr.Blocks() as demo:
demo.title = f"Dataset Quiz for {EXAM_DATASET_ID}"
# State variables
question_idx = gr.State(value=0)
user_answers = gr.State(value=[])
user_token = gr.State(value=None)
with gr.Row(variant="compact"):
gr.Markdown(f"## Welcome to the {EXAM_DATASET_ID} Quiz")
with gr.Row(variant="compact"):
gr.Markdown(
"- Log in first, then click 'Start' to begin. \n- Answer each question, click 'Next' \n- click 'Submit' to publish your results to the Hugging Face Hub."
)
with gr.Row(variant="panel"):
question_text = gr.Markdown("")
radio_choices = gr.Radio(
choices=[], label="Your Answer", scale=1, visible=False
)
with gr.Row(variant="compact"):
status_text = gr.Markdown("")
certificate_img = gr.Image(type="pil", visible=False)
linkedin_btn = gr.HTML(
visible=True,
value="""
🔒 Login Required
Please log in with your Hugging Face account to access the quiz and earn your LinkedIn certificate!
""",
)
with gr.Row(variant="compact"):
login_btn = gr.LoginButton(visible=True)
start_btn = gr.Button("Start ⏭️", visible=True)
next_btn = gr.Button("Next ⏭️", visible=False)
submit_btn = gr.Button("🎓 Get your certificate", visible=False)
with gr.Row(variant="panel"):
custom_name_input = gr.Textbox(
label="Custom Name for Certificate",
placeholder="Enter name as you want it to appear on the certificate",
info="Leave empty to use your Hugging Face profile name",
visible=False,
value=None,
)
# Wire up the event handlers
login_btn.click(
fn=on_user_logged_in,
inputs=None,
outputs=[
login_btn,
start_btn,
next_btn,
submit_btn,
question_text,
radio_choices,
status_text,
question_idx,
user_answers,
certificate_img,
linkedin_btn,
user_token,
],
)
start_btn.click(
fn=handle_quiz,
inputs=[question_idx, user_answers, gr.State(""), gr.State(True)],
outputs=[
question_text,
radio_choices,
status_text,
question_idx,
user_answers,
start_btn,
next_btn,
submit_btn,
certificate_img,
linkedin_btn,
],
)
next_btn.click(
fn=handle_quiz,
inputs=[question_idx, user_answers, radio_choices, gr.State(False)],
outputs=[
question_text,
radio_choices,
status_text,
question_idx,
user_answers,
start_btn,
next_btn,
submit_btn,
certificate_img,
linkedin_btn,
],
)
submit_btn.click(
fn=push_results_to_hub,
inputs=[
user_answers,
custom_name_input,
],
outputs=[
status_text,
certificate_img,
linkedin_btn,
custom_name_input,
],
)
custom_name_input.submit(
fn=push_results_to_hub,
inputs=[user_answers, custom_name_input],
outputs=[status_text, certificate_img, linkedin_btn, custom_name_input],
)
if __name__ == "__main__":
# Note: If testing locally, you'll need to run `huggingface-cli login` or set HF_TOKEN
# environment variable for the login to work locally.
demo.queue() # Enable queuing for async operations
demo.launch()