diff --git a/.gitignore b/.gitignore index c2efc04c..24ce1529 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,10 @@ sites/*/static/external_cache/ # ============================================================= # Intermediate / volatile — never committed anywhere. # ============================================================= -sites/*/scraped_data/ # scrape pipeline intermediate; runtime data lives in instance_seed/*.db -sites/*/instance/ # rebuilt at every container boot from instance_seed/ +# scrape pipeline intermediate; runtime data lives in instance_seed/*.db +sites/*/scraped_data/ +# rebuilt at every container boot from instance_seed/ +sites/*/instance/ sites/*/venv/ # HF download metadata produced by `hf download`. diff --git a/Dockerfile b/Dockerfile index 991e5ab6..f653515e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,6 @@ COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py RUN chmod +x /opt/websyn_start.sh -EXPOSE 8101 40000-40014 +EXPOSE 8101 40000-40015 CMD ["/opt/websyn_start.sh"] diff --git a/control_server.py b/control_server.py index c255253c..b078b8d3 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', + 'coursera', 'espn', 'kaggle', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/sites/kaggle/_health.py b/sites/kaggle/_health.py new file mode 100644 index 00000000..e8a5f701 --- /dev/null +++ b/sites/kaggle/_health.py @@ -0,0 +1,45 @@ +"""kaggle mirror health check.""" +from healthcheck import random_user + + +def run(p): + p.assert_get('home', '/', must_contain='Kaggle') + p.assert_get('competitions', '/competitions', must_contain='Competitions') + p.assert_get('search fraud', '/search?q=fraud', must_contain='fraud') + p.assert_get( + 'competition detail', + '/competitions/credit-default-risk-2026', + must_contain='ROC AUC', + ) + p.assert_get('dataset detail', '/datasets/credit-card-fraud-transactions', + must_contain='About this Dataset') + p.assert_get('rankings', '/rankings', must_contain='Rankings') + + user = random_user() + html = p.assert_get('register page', '/register') + token = p.csrf(html) + if not token: + p.check('register csrf', False, 'no csrf') + return + p.assert_post('register submit', '/register', { + 'csrf_token': token, + 'username': user['username'], + 'email': user['email'], + 'password': user['password'], + 'confirm': user['password'], + }, accept_status=(200, 302, 303)) + + p.get('/logout') + + html = p.assert_get('login page', '/login') + token = p.csrf(html) + if not token: + p.check('login csrf', False, 'no csrf') + return + p.assert_post('login submit', '/login', { + 'csrf_token': token, + 'email': user['email'], + 'password': user['password'], + }, accept_status=(200, 302, 303)) + + p.assert_get('account page', '/account', accept_status=(200, 302)) diff --git a/sites/kaggle/app.py b/sites/kaggle/app.py new file mode 100644 index 00000000..bb44fa49 --- /dev/null +++ b/sites/kaggle/app.py @@ -0,0 +1,1397 @@ +""" +Kaggle Mirror — Full-stack Flask application. + +A deterministic offline mirror of kaggle.com for web-agent benchmarks. + +Entity model (data-science competition platform): + User — competitor with progression tiers + performance medals + Competition — hosted ML challenge (prize, metric, deadline, leaderboard) + Submission — a leaderboard entry for a competition + CompetitionEntry — a user joining a competition (the "register"/transaction flow) + Dataset — community dataset with usability score, votes, downloads + Notebook — public code (kernel), optionally linked to comp/dataset + Model — pretrained model with framework + variations + Course — Kaggle Learn micro-course + Discussion — forum thread; Comment — replies + Vote / Bookmark — polymorphic interactions across entity types + Follow — social graph (user -> user) +""" +import json +import os +import re +from datetime import datetime, date +from pathlib import Path + +# ---------------------------------------------------------------------------- +# Pinned mirror clock. The image is built once and evaluated at any future +# point; freeze "today" so date-relative tasks ("active competitions", +# "deadline this month") behave deterministically. Chosen to sit just after +# the newest seeded timestamp so recent uploads stay "recent" and every +# historical anchor stays firmly in the past. +# ---------------------------------------------------------------------------- +MIRROR_REFERENCE_DATE = datetime(2026, 6, 22, 12, 0, 0) + + +def mirror_now() -> datetime: + return MIRROR_REFERENCE_DATE + + +def mirror_today() -> date: + return MIRROR_REFERENCE_DATE.date() + + +from flask import (Flask, render_template, request, redirect, url_for, flash, + jsonify, abort) +from flask_sqlalchemy import SQLAlchemy +from flask_login import (LoginManager, UserMixin, login_user, logout_user, + login_required, current_user) +from flask_bcrypt import Bcrypt +from flask_wtf import FlaskForm, CSRFProtect +from wtforms import (StringField, PasswordField, TextAreaField, SelectField, + HiddenField) +from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional as OptionalV + +from seed_data import ( + PERFORMANCE_TIERS, COMPETITION_CATEGORIES, DISCUSSION_FORUMS, ML_FRAMEWORKS, + LICENSES, PROGRAMMING_LANGUAGES, BENCHMARK_USERS, NOTABLE_USERS, COMPETITIONS, + LEADERBOARDS, DATASETS, NOTEBOOKS, MODELS, COURSES, DISCUSSIONS, + DISCUSSION_COMMENTS, +) + +# ---------------------------------------------------------------------------- +# Flask setup +# ---------------------------------------------------------------------------- +ROOT = Path(__file__).parent +app = Flask(__name__) +app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "kaggle-mirror-dev-secret-key-change-me") +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{ROOT / 'instance' / 'kaggle.db'}" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +(ROOT / "instance").mkdir(exist_ok=True) + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +csrf = CSRFProtect(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message_category = "info" + +TIER_COLORS = { + "Novice": "#5ac995", "Contributor": "#00aaff", "Expert": "#95319b", + "Master": "#f96517", "Grandmaster": "#dca917", +} +MEDAL_EMOJI = {"gold": "🥇", "silver": "🥈", "bronze": "🥉"} + + +@app.template_filter("fromjson") +def _fromjson_filter(value): + try: + if isinstance(value, (list, dict)): + return value + return json.loads(value) + except Exception: + return [] + + +@app.template_filter("commafy") +def _commafy(value): + try: + return f"{int(value):,}" + except Exception: + return value + + +@app.template_filter("reltime") +def _reltime(value): + """Human 'time ago' relative to the mirror clock.""" + if not value: + return "" + if isinstance(value, str): + try: + value = datetime.fromisoformat(value) + except Exception: + return value + if isinstance(value, date) and not isinstance(value, datetime): + value = datetime(value.year, value.month, value.day) + delta = mirror_now() - value + days = delta.days + if days < 0: + return "just now" + if days == 0: + return "today" + if days == 1: + return "yesterday" + if days < 30: + return f"{days} days ago" + if days < 365: + return f"{days // 30} months ago" + return f"{days // 365} years ago" + + +# ------------------------------------------------------------ +# Database models +# ------------------------------------------------------------ +class User(db.Model, UserMixin): + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(180), unique=True, nullable=False, index=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(200), nullable=False) + display_name = db.Column(db.String(120), default="") + bio = db.Column(db.Text, default="") + avatar_url = db.Column(db.String(300), default="/static/images/avatars/default.png") + location = db.Column(db.String(120), default="") + occupation = db.Column(db.String(120), default="") + organization = db.Column(db.String(120), default="") + website = db.Column(db.String(200), default="") + tier = db.Column(db.String(20), default="Novice") + tiers_json = db.Column(db.Text, default="{}") # per-category tiers + points = db.Column(db.Integer, default=0) + gold = db.Column(db.Integer, default=0) + silver = db.Column(db.Integer, default=0) + bronze = db.Column(db.Integer, default=0) + comp_rank = db.Column(db.Integer, nullable=True) # global competitions rank + is_org = db.Column(db.Boolean, default=False) + is_admin = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=mirror_now) + + @property + def tier_color(self): + return TIER_COLORS.get(self.tier, "#888") + + @property + def category_tiers(self): + try: + return json.loads(self.tiers_json or "{}") + except Exception: + return {} + + @property + def total_medals(self): + return (self.gold or 0) + (self.silver or 0) + (self.bronze or 0) + + +class Competition(db.Model): + __tablename__ = "competitions" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + subtitle = db.Column(db.String(300), default="") + category = db.Column(db.String(40), index=True) + host = db.Column(db.String(160), default="Kaggle") + owner_username = db.Column(db.String(80), default="Kaggle") + reward = db.Column(db.String(60), default="Knowledge") + reward_value = db.Column(db.Integer, default=0) # USD, 0 for non-cash + metric = db.Column(db.String(80), default="") + num_teams = db.Column(db.Integer, default=0) + deadline = db.Column(db.Date, nullable=True) + tags_json = db.Column(db.Text, default="[]") + thumbnail = db.Column(db.String(120), default="") + description = db.Column(db.Text, default="") + + submissions = db.relationship("Submission", backref="competition", + cascade="all, delete-orphan", lazy="dynamic") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + @property + def is_active(self): + return self.deadline is None or self.deadline >= mirror_today() + + @property + def days_left(self): + if not self.deadline: + return None + return (self.deadline - mirror_today()).days + + @property + def reward_display(self): + return self.reward + + +class Submission(db.Model): + __tablename__ = "submissions" + id = db.Column(db.Integer, primary_key=True) + competition_id = db.Column(db.Integer, db.ForeignKey("competitions.id"), index=True) + rank = db.Column(db.Integer) + team_name = db.Column(db.String(160)) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + score = db.Column(db.Float) + submitted_at = db.Column(db.Date) + + +class CompetitionEntry(db.Model): + """A user joining a competition — the register/transaction flow.""" + __tablename__ = "competition_entries" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + competition_id = db.Column(db.Integer, db.ForeignKey("competitions.id"), index=True) + team_name = db.Column(db.String(160), default="") + accepted_rules = db.Column(db.Boolean, default=True) + joined_at = db.Column(db.DateTime, default=mirror_now) + competition = db.relationship("Competition") + + +class Dataset(db.Model): + __tablename__ = "datasets" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + subtitle = db.Column(db.String(300), default="") + owner_username = db.Column(db.String(80), default="") + size = db.Column(db.String(40), default="") + size_bytes = db.Column(db.BigInteger, default=0) + file_count = db.Column(db.Integer, default=1) + file_types = db.Column(db.String(120), default="CSV") + usability = db.Column(db.Float, default=0.0) + upvotes = db.Column(db.Integer, default=0) + downloads = db.Column(db.Integer, default=0) + views = db.Column(db.Integer, default=0) + license = db.Column(db.String(120), default="") + tags_json = db.Column(db.Text, default="[]") + last_updated = db.Column(db.Date, nullable=True) + thumbnail = db.Column(db.String(120), default="") + description = db.Column(db.Text, default="") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + +class Notebook(db.Model): + __tablename__ = "notebooks" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + author_username = db.Column(db.String(80), default="") + language = db.Column(db.String(20), default="Python") + votes = db.Column(db.Integer, default=0) + comments = db.Column(db.Integer, default=0) + medal = db.Column(db.String(10), nullable=True) + best_score = db.Column(db.String(40), nullable=True) + runtime = db.Column(db.String(40), default="") + last_run = db.Column(db.Date, nullable=True) + linked_competition = db.Column(db.String(120), nullable=True) + linked_dataset = db.Column(db.String(120), nullable=True) + thumbnail = db.Column(db.String(120), default="") + tags_json = db.Column(db.Text, default="[]") + description = db.Column(db.Text, default="") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + @property + def medal_emoji(self): + return MEDAL_EMOJI.get(self.medal, "") + + +class Model(db.Model): + __tablename__ = "models" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + owner_username = db.Column(db.String(80), default="") + framework = db.Column(db.String(40), default="") + variations = db.Column(db.Integer, default=1) + downloads = db.Column(db.Integer, default=0) + upvotes = db.Column(db.Integer, default=0) + license = db.Column(db.String(120), default="") + tags_json = db.Column(db.Text, default="[]") + last_updated = db.Column(db.Date, nullable=True) + thumbnail = db.Column(db.String(120), default="") + description = db.Column(db.Text, default="") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + +class Course(db.Model): + __tablename__ = "courses" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(160), nullable=False) + lessons = db.Column(db.Integer, default=0) + hours = db.Column(db.Integer, default=0) + level = db.Column(db.String(20), default="Beginner") + icon = db.Column(db.String(10), default="📘") + tags_json = db.Column(db.Text, default="[]") + description = db.Column(db.Text, default="") + lesson_titles_json = db.Column(db.Text, default="[]") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + @property + def lesson_titles(self): + try: + return json.loads(self.lesson_titles_json or "[]") + except Exception: + return [] + + +class Discussion(db.Model): + __tablename__ = "discussions" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(140), unique=True, nullable=False, index=True) + title = db.Column(db.String(240), nullable=False) + author_username = db.Column(db.String(80), default="") + forum = db.Column(db.String(60), index=True) + votes = db.Column(db.Integer, default=0) + comment_count = db.Column(db.Integer, default=0) + pinned = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=mirror_now) + body = db.Column(db.Text, default="") + + comments = db.relationship("Comment", backref="discussion", + cascade="all, delete-orphan", lazy="dynamic") + + +class Comment(db.Model): + __tablename__ = "comments" + id = db.Column(db.Integer, primary_key=True) + discussion_id = db.Column(db.Integer, db.ForeignKey("discussions.id"), index=True) + author_username = db.Column(db.String(80), default="") + body = db.Column(db.Text, default="") + votes = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=mirror_now) + + +class Vote(db.Model): + """Polymorphic upvote: entity_type in {dataset, notebook, model, discussion, competition}.""" + __tablename__ = "votes" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + entity_type = db.Column(db.String(20), index=True) + entity_id = db.Column(db.Integer, index=True) + created_at = db.Column(db.DateTime, default=mirror_now) + + +class Bookmark(db.Model): + __tablename__ = "bookmarks" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + entity_type = db.Column(db.String(20), index=True) + entity_id = db.Column(db.Integer, index=True) + created_at = db.Column(db.DateTime, default=mirror_now) + + +class Follow(db.Model): + __tablename__ = "follows" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + target_username = db.Column(db.String(80), index=True) + created_at = db.Column(db.DateTime, default=mirror_now) + + +@login_manager.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + + +# ------------------------------------------------------------ +# Forms +# ------------------------------------------------------------ +class LoginForm(FlaskForm): + email = StringField("Email", validators=[DataRequired(), Email()]) + password = PasswordField("Password", validators=[DataRequired()]) + + +class RegisterForm(FlaskForm): + email = StringField("Email", validators=[DataRequired(), Email()]) + username = StringField("Username", validators=[DataRequired(), Length(min=3, max=40)]) + password = PasswordField("Password", validators=[DataRequired(), Length(min=6)]) + confirm = PasswordField("Confirm password", validators=[DataRequired(), EqualTo("password")]) + + +class ProfileForm(FlaskForm): + display_name = StringField("Display name", validators=[OptionalV(), Length(max=120)]) + bio = TextAreaField("Bio", validators=[OptionalV(), Length(max=600)]) + location = StringField("Location", validators=[OptionalV(), Length(max=120)]) + occupation = StringField("Occupation", validators=[OptionalV(), Length(max=120)]) + organization = StringField("Organization", validators=[OptionalV(), Length(max=120)]) + website = StringField("Website", validators=[OptionalV(), Length(max=200)]) + + +class PasswordForm(FlaskForm): + current = PasswordField("Current password", validators=[DataRequired()]) + new = PasswordField("New password", validators=[DataRequired(), Length(min=6)]) + confirm = PasswordField("Confirm new password", validators=[DataRequired(), EqualTo("new")]) + + +class JoinCompetitionForm(FlaskForm): + team_name = StringField("Team name", validators=[DataRequired(), Length(max=120)]) + accept_rules = HiddenField() + + +class DiscussionForm(FlaskForm): + title = StringField("Title", validators=[DataRequired(), Length(max=240)]) + forum = SelectField("Forum", choices=[(f, f) for f in DISCUSSION_FORUMS], validators=[DataRequired()]) + body = TextAreaField("Body", validators=[DataRequired(), Length(min=10)]) + + +class CommentForm(FlaskForm): + body = TextAreaField("Comment", validators=[DataRequired(), Length(min=1)]) + + +# ------------------------------------------------------------ +# Search scoring (token-overlap, NOT strict AND) +# ------------------------------------------------------------ +STOPWORDS = {"the", "a", "an", "of", "for", "to", "and", "or", "in", "on", "with", + "by", "is", "are", "how", "what", "best", "top", "find", "show", "me"} + + +def _tokenize(q: str): + return [t for t in re.findall(r"[a-z0-9.+#-]+", (q or "").lower()) + if len(t) >= 2 and t not in STOPWORDS] + + +def _score(hay: str, tokens): + if not tokens: + return 1 + hay = hay.lower() + return sum(1 for t in tokens if t in hay) + + +def _comp_hay(c): + return " ".join([c.slug, c.title, c.subtitle or "", c.category or "", + c.host or "", c.metric or "", c.description or "", + " ".join(c.tags)]) + + +def _dataset_hay(d): + return " ".join([d.slug, d.title, d.subtitle or "", d.owner_username or "", + d.license or "", d.description or "", d.file_types or "", + " ".join(d.tags)]) + + +def _notebook_hay(n): + return " ".join([n.slug, n.title, n.author_username or "", n.language or "", + n.description or "", n.linked_competition or "", + n.linked_dataset or "", " ".join(n.tags)]) + + +def _model_hay(m): + return " ".join([m.slug, m.title, m.owner_username or "", m.framework or "", + m.license or "", m.description or "", " ".join(m.tags)]) + + +def _user_hay(u): + return " ".join([u.username, u.display_name or "", u.bio or "", + u.location or "", u.occupation or "", u.organization or "", + u.tier or ""]) + + +def _discussion_hay(d): + return " ".join([d.slug, d.title, d.author_username or "", d.forum or "", + d.body or ""]) + + +# ------------------------------------------------------------ +# Context helpers +# ------------------------------------------------------------ +@app.context_processor +def inject_globals(): + return dict( + current_year=mirror_now().year, + nav_forums=DISCUSSION_FORUMS, + comp_categories=COMPETITION_CATEGORIES, + TIER_COLORS=TIER_COLORS, + MEDAL_EMOJI=MEDAL_EMOJI, + ) + + +def user_by_name(username): + return User.query.filter_by(username=username).first() + + +def has_voted(entity_type, entity_id): + if not current_user.is_authenticated: + return False + return Vote.query.filter_by(user_id=current_user.id, entity_type=entity_type, + entity_id=entity_id).first() is not None + + +def has_bookmarked(entity_type, entity_id): + if not current_user.is_authenticated: + return False + return Bookmark.query.filter_by(user_id=current_user.id, entity_type=entity_type, + entity_id=entity_id).first() is not None + + +def is_following(username): + if not current_user.is_authenticated: + return False + return Follow.query.filter_by(user_id=current_user.id, target_username=username).first() is not None + + +app.jinja_env.globals.update( + has_voted=has_voted, has_bookmarked=has_bookmarked, + is_following=is_following, user_by_name=user_by_name, +) + + +# ------------------------------------------------------------ +# Homepage + static pages +# ------------------------------------------------------------ +@app.route("/") +def index(): + featured = Competition.query.filter(Competition.category.in_(["Featured", "Research"])) \ + .order_by(Competition.reward_value.desc()).limit(4).all() + active_comps = Competition.query.order_by(Competition.num_teams.desc()).limit(6).all() + hot_datasets = Dataset.query.order_by(Dataset.upvotes.desc()).limit(6).all() + trending_notebooks = Notebook.query.order_by(Notebook.votes.desc()).limit(4).all() + courses = Course.query.limit(4).all() + return render_template("index.html", featured=featured, active_comps=active_comps, + hot_datasets=hot_datasets, trending_notebooks=trending_notebooks, + courses=courses) + + +@app.route("/_health") +def health(): + return {"ok": True, "site": "kaggle", + "competitions": Competition.query.count(), + "datasets": Dataset.query.count()} + + +# ------------------------------------------------------------ +# Competitions +# ------------------------------------------------------------ +@app.route("/competitions") +def competitions_list(): + q = request.args.get("q", "").strip() + category = request.args.get("category", "").strip() + status = request.args.get("status", "").strip() # active | completed + sort = request.args.get("sort", "relevance" if q else "teams") + + query = Competition.query + if category: + query = query.filter(Competition.category == category) + comps = query.all() + + if status == "active": + comps = [c for c in comps if c.is_active] + elif status == "completed": + comps = [c for c in comps if not c.is_active] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, c) for c in comps if (s := _score(_comp_hay(c), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].num_teams)) + comps = [c for _, c in scored] + else: + if sort == "prize": + comps.sort(key=lambda c: -c.reward_value) + elif sort == "deadline": + comps.sort(key=lambda c: (c.deadline or date.max)) + elif sort == "newest": + comps.sort(key=lambda c: -c.id) + else: # teams + comps.sort(key=lambda c: -c.num_teams) + + return render_template("competitions.html", comps=comps, q=q, + category=category, status=status, sort=sort) + + +@app.route("/competitions/") +def competition_detail(slug): + c = Competition.query.filter_by(slug=slug).first_or_404() + leaderboard = c.submissions.order_by(Submission.rank.asc()).all() + host_user = user_by_name(c.owner_username) + notebooks = Notebook.query.filter_by(linked_competition=slug).order_by(Notebook.votes.desc()).all() + entry = None + joined = False + if current_user.is_authenticated: + entry = CompetitionEntry.query.filter_by(user_id=current_user.id, competition_id=c.id).first() + joined = entry is not None + join_form = JoinCompetitionForm() + return render_template("competition_detail.html", c=c, leaderboard=leaderboard, + host_user=host_user, notebooks=notebooks, joined=joined, + entry=entry, join_form=join_form, tab=request.args.get("tab", "overview")) + + +@app.route("/competitions//leaderboard") +def competition_leaderboard(slug): + c = Competition.query.filter_by(slug=slug).first_or_404() + leaderboard = c.submissions.order_by(Submission.rank.asc()).all() + return render_template("leaderboard.html", c=c, leaderboard=leaderboard) + + +@app.route("/competitions//join", methods=["POST"]) +@login_required +def competition_join(slug): + c = Competition.query.filter_by(slug=slug).first_or_404() + if not c.is_active: + flash("This competition has closed — you can no longer join.", "error") + return redirect(url_for("competition_detail", slug=slug)) + form = JoinCompetitionForm() + if form.validate_on_submit(): + existing = CompetitionEntry.query.filter_by(user_id=current_user.id, competition_id=c.id).first() + if existing: + flash("You have already joined this competition.", "info") + else: + entry = CompetitionEntry(user_id=current_user.id, competition_id=c.id, + team_name=form.team_name.data.strip(), accepted_rules=True) + db.session.add(entry) + c.num_teams = (c.num_teams or 0) + 1 + db.session.commit() + flash(f"You're in! Team '{entry.team_name}' joined {c.title}.", "success") + else: + flash("Please enter a team name and accept the rules.", "error") + return redirect(url_for("competition_detail", slug=slug)) + + +# ------------------------------------------------------------ +# Datasets +# ------------------------------------------------------------ +@app.route("/datasets") +def datasets_list(): + q = request.args.get("q", "").strip() + tag = request.args.get("tag", "").strip() + file_type = request.args.get("file_type", "").strip() + sort = request.args.get("sort", "relevance" if q else "hottest") + + datasets = Dataset.query.all() + if tag: + datasets = [d for d in datasets if tag in d.tags] + if file_type: + datasets = [d for d in datasets if file_type.lower() in (d.file_types or "").lower()] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, d) for d in datasets if (s := _score(_dataset_hay(d), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].upvotes)) + datasets = [d for _, d in scored] + else: + if sort == "votes": + datasets.sort(key=lambda d: -d.upvotes) + elif sort == "downloads": + datasets.sort(key=lambda d: -d.downloads) + elif sort == "usability": + datasets.sort(key=lambda d: -d.usability) + elif sort == "updated": + datasets.sort(key=lambda d: (d.last_updated or date.min), reverse=True) + elif sort == "size": + datasets.sort(key=lambda d: -d.size_bytes) + else: # hottest + datasets.sort(key=lambda d: -(d.upvotes + d.downloads // 20)) + + all_tags = sorted({t for d in Dataset.query.all() for t in d.tags}) + return render_template("datasets.html", datasets=datasets, q=q, tag=tag, + file_type=file_type, sort=sort, all_tags=all_tags) + + +@app.route("/datasets/") +def dataset_detail(slug): + d = Dataset.query.filter_by(slug=slug).first_or_404() + owner = user_by_name(d.owner_username) + notebooks = Notebook.query.filter_by(linked_dataset=slug).order_by(Notebook.votes.desc()).all() + return render_template("dataset_detail.html", d=d, owner=owner, notebooks=notebooks) + + +@app.route("/datasets//download") +def dataset_download(slug): + d = Dataset.query.filter_by(slug=slug).first_or_404() + d.downloads = (d.downloads or 0) + 1 + db.session.commit() + flash(f"Download started: {d.title} ({d.size}).", "success") + return redirect(url_for("dataset_detail", slug=slug)) + + +# ------------------------------------------------------------ +# Notebooks (Code) +# ------------------------------------------------------------ +@app.route("/code") +def notebooks_list(): + q = request.args.get("q", "").strip() + language = request.args.get("language", "").strip() + medal = request.args.get("medal", "").strip() + sort = request.args.get("sort", "relevance" if q else "votes") + + notebooks = Notebook.query.all() + if language: + notebooks = [n for n in notebooks if n.language == language] + if medal: + notebooks = [n for n in notebooks if n.medal == medal] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, n) for n in notebooks if (s := _score(_notebook_hay(n), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].votes)) + notebooks = [n for _, n in scored] + else: + if sort == "comments": + notebooks.sort(key=lambda n: -n.comments) + elif sort == "recent": + notebooks.sort(key=lambda n: (n.last_run or date.min), reverse=True) + else: + notebooks.sort(key=lambda n: -n.votes) + + return render_template("notebooks.html", notebooks=notebooks, q=q, + language=language, medal=medal, sort=sort) + + +@app.route("/code/") +def notebook_detail(slug): + n = Notebook.query.filter_by(slug=slug).first_or_404() + author = user_by_name(n.author_username) + comp = Competition.query.filter_by(slug=n.linked_competition).first() if n.linked_competition else None + dataset = Dataset.query.filter_by(slug=n.linked_dataset).first() if n.linked_dataset else None + return render_template("notebook_detail.html", n=n, author=author, comp=comp, dataset=dataset) + + +# ------------------------------------------------------------ +# Models +# ------------------------------------------------------------ +@app.route("/models") +def models_list(): + q = request.args.get("q", "").strip() + framework = request.args.get("framework", "").strip() + sort = request.args.get("sort", "relevance" if q else "downloads") + + models = Model.query.all() + if framework: + models = [m for m in models if m.framework == framework] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, m) for m in models if (s := _score(_model_hay(m), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].downloads)) + models = [m for _, m in scored] + else: + if sort == "votes": + models.sort(key=lambda m: -m.upvotes) + elif sort == "updated": + models.sort(key=lambda m: (m.last_updated or date.min), reverse=True) + else: + models.sort(key=lambda m: -m.downloads) + + return render_template("models.html", models=models, q=q, + framework=framework, sort=sort, frameworks=ML_FRAMEWORKS) + + +@app.route("/models/") +def model_detail(slug): + m = Model.query.filter_by(slug=slug).first_or_404() + owner = user_by_name(m.owner_username) + return render_template("model_detail.html", m=m, owner=owner) + + +# ------------------------------------------------------------ +# Learn (courses) +# ------------------------------------------------------------ +@app.route("/learn") +def learn(): + courses = Course.query.all() + return render_template("learn.html", courses=courses) + + +@app.route("/learn/") +def course_detail(slug): + course = Course.query.filter_by(slug=slug).first_or_404() + return render_template("course_detail.html", course=course) + + +# ------------------------------------------------------------ +# Discussions +# ------------------------------------------------------------ +@app.route("/discussions") +def discussions_list(): + q = request.args.get("q", "").strip() + forum = request.args.get("forum", "").strip() + sort = request.args.get("sort", "relevance" if q else "hot") + + discussions = Discussion.query.all() + if forum: + discussions = [d for d in discussions if d.forum == forum] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, d) for d in discussions if (s := _score(_discussion_hay(d), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].votes)) + discussions = [d for _, d in scored] + else: + if sort == "recent": + discussions.sort(key=lambda d: d.created_at or mirror_now(), reverse=True) + elif sort == "comments": + discussions.sort(key=lambda d: -d.comment_count) + else: # hot — pinned first, then votes + discussions.sort(key=lambda d: (not d.pinned, -d.votes)) + + return render_template("discussions.html", discussions=discussions, q=q, + forum=forum, sort=sort) + + +@app.route("/discussions/") +def discussion_detail(slug): + d = Discussion.query.filter_by(slug=slug).first_or_404() + author = user_by_name(d.author_username) + comments = d.comments.order_by(Comment.votes.desc()).all() + comment_form = CommentForm() + return render_template("discussion_detail.html", d=d, author=author, + comments=comments, comment_form=comment_form) + + +@app.route("/discussions/new", methods=["GET", "POST"]) +@login_required +def discussion_new(): + form = DiscussionForm() + if form.validate_on_submit(): + base = re.sub(r"[^a-z0-9]+", "-", form.title.data.lower()).strip("-")[:120] or "thread" + slug = base + n = 2 + while Discussion.query.filter_by(slug=slug).first(): + slug = f"{base}-{n}" + n += 1 + d = Discussion(slug=slug, title=form.title.data.strip(), forum=form.forum.data, + author_username=current_user.username, body=form.body.data.strip(), + votes=0, comment_count=0, pinned=False, created_at=mirror_now()) + db.session.add(d) + db.session.commit() + flash("Discussion posted.", "success") + return redirect(url_for("discussion_detail", slug=slug)) + return render_template("discussion_new.html", form=form) + + +@app.route("/discussions//comment", methods=["POST"]) +@login_required +def discussion_comment(slug): + d = Discussion.query.filter_by(slug=slug).first_or_404() + form = CommentForm() + if form.validate_on_submit(): + c = Comment(discussion_id=d.id, author_username=current_user.username, + body=form.body.data.strip(), votes=0, created_at=mirror_now()) + db.session.add(c) + d.comment_count = (d.comment_count or 0) + 1 + db.session.commit() + flash("Comment posted.", "success") + else: + flash("Comment cannot be empty.", "error") + return redirect(url_for("discussion_detail", slug=slug)) + + +# ------------------------------------------------------------ +# Rankings + user profiles +# ------------------------------------------------------------ +@app.route("/rankings") +def rankings(): + category = request.args.get("category", "competitions") + users = User.query.filter_by(is_org=False).all() + tier_weight = {t: i for i, t in enumerate(PERFORMANCE_TIERS)} + + def keyf(u): + ct = u.category_tiers.get(category, u.tier) + return (-tier_weight.get(ct, 0), -(u.points or 0)) + + users.sort(key=keyf) + return render_template("rankings.html", users=users, category=category, + categories=["competitions", "datasets", "notebooks", "discussions"]) + + +@app.route("/user/") +def user_profile(username): + u = User.query.filter_by(username=username).first_or_404() + datasets = Dataset.query.filter_by(owner_username=username).order_by(Dataset.upvotes.desc()).all() + notebooks = Notebook.query.filter_by(author_username=username).order_by(Notebook.votes.desc()).all() + models = Model.query.filter_by(owner_username=username).order_by(Model.downloads.desc()).all() + discussions = Discussion.query.filter_by(author_username=username).order_by(Discussion.votes.desc()).all() + hosted = Competition.query.filter_by(owner_username=username).all() + follower_count = Follow.query.filter_by(target_username=username).count() + return render_template("user_profile.html", u=u, datasets=datasets, notebooks=notebooks, + models=models, discussions=discussions, hosted=hosted, + follower_count=follower_count) + + +# ------------------------------------------------------------ +# Global search +# ------------------------------------------------------------ +@app.route("/search") +def search(): + q = request.args.get("q", "").strip() + scope = request.args.get("type", "all") + tokens = _tokenize(q) + min_req = max(1, len(tokens) // 2) if tokens else 0 + + def run(items, hayf, sortkey): + if not tokens: + return [] + scored = [(s, it) for it in items if (s := _score(hayf(it), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], sortkey(sc[1]))) + return [it for _, it in scored] + + comps = run(Competition.query.all(), _comp_hay, lambda c: -c.num_teams) if scope in ("all", "competitions") else [] + datasets = run(Dataset.query.all(), _dataset_hay, lambda d: -d.upvotes) if scope in ("all", "datasets") else [] + notebooks = run(Notebook.query.all(), _notebook_hay, lambda n: -n.votes) if scope in ("all", "code") else [] + models = run(Model.query.all(), _model_hay, lambda m: -m.downloads) if scope in ("all", "models") else [] + users = run(User.query.all(), _user_hay, lambda u: -(u.points or 0)) if scope in ("all", "users") else [] + discussions = run(Discussion.query.all(), _discussion_hay, lambda d: -d.votes) if scope in ("all", "discussions") else [] + + total = len(comps) + len(datasets) + len(notebooks) + len(models) + len(users) + len(discussions) + return render_template("search.html", q=q, scope=scope, total=total, + comps=comps, datasets=datasets, notebooks=notebooks, + models=models, users=users, discussions=discussions) + + +# ------------------------------------------------------------ +# Auth +# ------------------------------------------------------------ +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("index")) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data.lower().strip()).first() + if user and bcrypt.check_password_hash(user.password_hash, form.password.data): + login_user(user) + flash("Welcome back!", "success") + return redirect(request.args.get("next") or url_for("index")) + flash("Invalid email or password.", "error") + return render_template("login.html", form=form) + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if current_user.is_authenticated: + return redirect(url_for("index")) + form = RegisterForm() + if form.validate_on_submit(): + email = form.email.data.lower().strip() + username = form.username.data.strip() + if User.query.filter_by(email=email).first(): + flash("That email is already registered.", "error") + elif User.query.filter_by(username=username).first(): + flash("That username is taken.", "error") + else: + user = User( + email=email, username=username, + password_hash=bcrypt.generate_password_hash(form.password.data).decode(), + display_name=username, tier="Novice", + tiers_json=json.dumps({"competitions": "Novice", "datasets": "Novice", + "notebooks": "Novice", "discussions": "Novice"}), + avatar_url="/static/images/avatars/default.png", + created_at=mirror_now(), + ) + db.session.add(user) + db.session.commit() + login_user(user) + flash("Welcome to Kaggle!", "success") + return redirect(url_for("index")) + return render_template("register.html", form=form) + + +@app.route("/logout") +def logout(): + logout_user() + flash("Signed out.", "info") + return redirect(url_for("index")) + + +@app.route("/account") +@login_required +def account(): + entries = CompetitionEntry.query.filter_by(user_id=current_user.id).order_by(CompetitionEntry.joined_at.desc()).all() + bookmarks = Bookmark.query.filter_by(user_id=current_user.id).all() + votes = Vote.query.filter_by(user_id=current_user.id).all() + follows = Follow.query.filter_by(user_id=current_user.id).all() + bm = [(_resolve_entity(b.entity_type, b.entity_id), b) for b in bookmarks] + bm = [(e, b) for e, b in bm if e is not None] + return render_template("account.html", entries=entries, bookmarks=bm, + votes=votes, follows=follows) + + +@app.route("/account/edit", methods=["GET", "POST"]) +@login_required +def account_edit(): + form = ProfileForm(obj=current_user) + if form.validate_on_submit(): + current_user.display_name = form.display_name.data + current_user.bio = form.bio.data + current_user.location = form.location.data + current_user.occupation = form.occupation.data + current_user.organization = form.organization.data + current_user.website = form.website.data + db.session.commit() + flash("Profile updated.", "success") + return redirect(url_for("account")) + return render_template("account_edit.html", form=form) + + +@app.route("/account/password", methods=["GET", "POST"]) +@login_required +def change_password(): + form = PasswordForm() + if form.validate_on_submit(): + if not bcrypt.check_password_hash(current_user.password_hash, form.current.data): + flash("Current password incorrect.", "error") + else: + current_user.password_hash = bcrypt.generate_password_hash(form.new.data).decode() + db.session.commit() + flash("Password updated.", "success") + return redirect(url_for("account")) + return render_template("change_password.html", form=form) + + +@app.route("/account/delete", methods=["POST"]) +@login_required +def account_delete(): + user = db.session.get(User, current_user.id) + db.session.delete(user) + db.session.commit() + logout_user() + flash("Account deleted.", "info") + return redirect(url_for("index")) + + +# ------------------------------------------------------------ +# Interactions: vote / bookmark / follow (JSON + form fallback) +# ------------------------------------------------------------ +_ENTITY_MODELS = { + "competition": Competition, "dataset": Dataset, "notebook": Notebook, + "model": Model, "discussion": Discussion, +} +_VOTE_FIELD = {"competition": None, "dataset": "upvotes", "notebook": "votes", + "model": "upvotes", "discussion": "votes"} + + +def _resolve_entity(etype, eid): + model = _ENTITY_MODELS.get(etype) + if not model: + return None + return db.session.get(model, eid) + + +@app.route("/api/vote", methods=["POST"]) +@login_required +def api_vote(): + etype = request.form.get("entity_type") or (request.json or {}).get("entity_type") + eid = request.form.get("entity_id") or (request.json or {}).get("entity_id") + try: + eid = int(eid) + except (TypeError, ValueError): + return jsonify(error="bad entity_id"), 400 + if etype not in _ENTITY_MODELS: + return jsonify(error="bad entity_type"), 400 + ent = _resolve_entity(etype, eid) + if ent is None: + return jsonify(error="not found"), 404 + existing = Vote.query.filter_by(user_id=current_user.id, entity_type=etype, entity_id=eid).first() + field = _VOTE_FIELD.get(etype) + if existing: + db.session.delete(existing) + if field: + setattr(ent, field, max(0, (getattr(ent, field) or 0) - 1)) + voted = False + else: + db.session.add(Vote(user_id=current_user.id, entity_type=etype, entity_id=eid)) + if field: + setattr(ent, field, (getattr(ent, field) or 0) + 1) + voted = True + db.session.commit() + count = getattr(ent, field) if field else None + if request.is_json or request.headers.get("Accept", "").startswith("application/json"): + return jsonify(ok=True, voted=voted, count=count) + flash("Vote recorded." if voted else "Vote removed.", "success") + return redirect(request.referrer or url_for("index")) + + +@app.route("/api/bookmark", methods=["POST"]) +@login_required +def api_bookmark(): + etype = request.form.get("entity_type") or (request.json or {}).get("entity_type") + eid = request.form.get("entity_id") or (request.json or {}).get("entity_id") + try: + eid = int(eid) + except (TypeError, ValueError): + return jsonify(error="bad entity_id"), 400 + if etype not in _ENTITY_MODELS: + return jsonify(error="bad entity_type"), 400 + if _resolve_entity(etype, eid) is None: + return jsonify(error="not found"), 404 + existing = Bookmark.query.filter_by(user_id=current_user.id, entity_type=etype, entity_id=eid).first() + if existing: + db.session.delete(existing) + bookmarked = False + else: + db.session.add(Bookmark(user_id=current_user.id, entity_type=etype, entity_id=eid)) + bookmarked = True + db.session.commit() + if request.is_json or request.headers.get("Accept", "").startswith("application/json"): + return jsonify(ok=True, bookmarked=bookmarked) + flash("Saved." if bookmarked else "Removed from saved.", "success") + return redirect(request.referrer or url_for("index")) + + +@app.route("/api/follow", methods=["POST"]) +@login_required +def api_follow(): + username = request.form.get("username") or (request.json or {}).get("username") + target = User.query.filter_by(username=username).first() + if target is None: + return jsonify(error="not found"), 404 + if target.id == current_user.id: + return jsonify(error="cannot follow yourself"), 400 + existing = Follow.query.filter_by(user_id=current_user.id, target_username=username).first() + if existing: + db.session.delete(existing) + following = False + else: + db.session.add(Follow(user_id=current_user.id, target_username=username)) + following = True + db.session.commit() + if request.is_json or request.headers.get("Accept", "").startswith("application/json"): + return jsonify(ok=True, following=following) + flash("Followed." if following else "Unfollowed.", "success") + return redirect(request.referrer or url_for("user_profile", username=username)) + + +# ------------------------------------------------------------ +# Error handlers +# ------------------------------------------------------------ +@app.errorhandler(404) +def not_found(e): + return render_template("404.html"), 404 + + +@app.errorhandler(500) +def server_error(e): + return render_template("500.html"), 500 + + +# ------------------------------------------------------------ +# Seeding (idempotent — gate every function as a whole) +# ------------------------------------------------------------ +def _d(iso): + return date.fromisoformat(iso) if iso else None + + +def _dt(iso): + return datetime.fromisoformat(iso) if iso else mirror_now() + + +def seed_benchmark_users(): + if User.query.filter_by(email="alice.j@test.com").first(): + return + for u in BENCHMARK_USERS: + db.session.add(User( + email=u["email"], username=u["username"], + password_hash=bcrypt.generate_password_hash(u["password"]).decode(), + display_name=u["display_name"], bio=u["bio"], tier=u["tier"], + tiers_json=json.dumps(u["tiers"]), points=u["points"], + gold=u["gold"], silver=u["silver"], bronze=u["bronze"], + location=u["location"], occupation=u["occupation"], organization=u["organization"], + avatar_url=f"/static/images/avatars/{u['avatar']}.png", + created_at=_dt(u["joined"]), + )) + db.session.commit() + + +def seed_users(): + if User.query.filter_by(username="psi_grandmaster").first(): + return + for u in NOTABLE_USERS: + db.session.add(User( + email=f"{u['username']}@kaggle.test", username=u["username"], + password_hash=bcrypt.generate_password_hash("TestPass123!").decode(), + display_name=u["display_name"], bio=u["bio"], tier=u["tier"], + tiers_json=json.dumps(u["tiers"]), points=u["points"], + gold=u["gold"], silver=u["silver"], bronze=u["bronze"], + comp_rank=u.get("comp_rank"), + location=u["location"], occupation=u["occupation"], organization=u["organization"], + avatar_url=f"/static/images/avatars/{u['avatar']}.png", + is_org=u.get("is_org", False), created_at=_dt(u["joined"]), + )) + # The platform "Kaggle" host account. + db.session.add(User( + email="staff@kaggle.test", username="Kaggle", + password_hash=bcrypt.generate_password_hash("TestPass123!").decode(), + display_name="Kaggle", bio="Official Kaggle account.", tier="Grandmaster", + tiers_json=json.dumps({"competitions": "Grandmaster", "datasets": "Grandmaster", + "notebooks": "Grandmaster", "discussions": "Grandmaster"}), + points=0, gold=0, silver=0, bronze=0, is_org=True, + avatar_url="/static/images/avatars/kaggle.png", created_at=_dt("2010-04-01"), + )) + db.session.commit() + + +def seed_competitions(): + if Competition.query.count() > 0: + return + for c in COMPETITIONS: + db.session.add(Competition( + slug=c["slug"], title=c["title"], subtitle=c["subtitle"], + category=c["category"], host=c["host"], owner_username=c["owner"], + reward=c["reward"], reward_value=c["reward_value"], metric=c["metric"], + num_teams=c["num_teams"], deadline=_d(c["deadline"]), + tags_json=json.dumps(c["tags"]), thumbnail=c["thumbnail"], + description=c["description"], + )) + db.session.commit() + for slug, rows in LEADERBOARDS.items(): + comp = Competition.query.filter_by(slug=slug).first() + if not comp: + continue + for rank, team, uname, score, submitted in rows: + u = User.query.filter_by(username=uname).first() + db.session.add(Submission( + competition_id=comp.id, rank=rank, team_name=team, + user_id=u.id if u else None, score=score, submitted_at=_d(submitted), + )) + db.session.commit() + + +def seed_datasets(): + if Dataset.query.count() > 0: + return + for d in DATASETS: + db.session.add(Dataset( + slug=d["slug"], title=d["title"], subtitle=d["subtitle"], + owner_username=d["owner"], size=d["size"], size_bytes=d["size_bytes"], + file_count=d["file_count"], file_types=d["file_types"], usability=d["usability"], + upvotes=d["upvotes"], downloads=d["downloads"], views=d["views"], + license=d["license"], tags_json=json.dumps(d["tags"]), + last_updated=_d(d["last_updated"]), thumbnail=d["thumbnail"], + description=d["description"], + )) + db.session.commit() + + +def seed_notebooks(): + if Notebook.query.count() > 0: + return + for n in NOTEBOOKS: + db.session.add(Notebook( + slug=n["slug"], title=n["title"], author_username=n["author"], + language=n["language"], votes=n["votes"], comments=n["comments"], + medal=n["medal"], best_score=n["best_score"], runtime=n["runtime"], + last_run=_d(n["last_run"]), linked_competition=n["linked_competition"], + linked_dataset=n["linked_dataset"], thumbnail=n["thumbnail"], + tags_json=json.dumps(n["tags"]), description=n["description"], + )) + db.session.commit() + + +def seed_models(): + if Model.query.count() > 0: + return + for m in MODELS: + db.session.add(Model( + slug=m["slug"], title=m["title"], owner_username=m["owner"], + framework=m["framework"], variations=m["variations"], downloads=m["downloads"], + upvotes=m["upvotes"], license=m["license"], tags_json=json.dumps(m["tags"]), + last_updated=_d(m["last_updated"]), thumbnail=m["thumbnail"], + description=m["description"], + )) + db.session.commit() + + +def seed_courses(): + if Course.query.count() > 0: + return + for c in COURSES: + db.session.add(Course( + slug=c["slug"], title=c["title"], lessons=c["lessons"], hours=c["hours"], + level=c["level"], icon=c["icon"], tags_json=json.dumps(c["tags"]), + description=c["description"], lesson_titles_json=json.dumps(c["lesson_titles"]), + )) + db.session.commit() + + +def seed_discussions(): + if Discussion.query.count() > 0: + return + for d in DISCUSSIONS: + db.session.add(Discussion( + slug=d["slug"], title=d["title"], author_username=d["author"], + forum=d["forum"], votes=d["votes"], comment_count=d["comments"], + pinned=d["pinned"], created_at=_dt(d["created_at"]), body=d["body"], + )) + db.session.commit() + for slug, rows in DISCUSSION_COMMENTS.items(): + disc = Discussion.query.filter_by(slug=slug).first() + if not disc: + continue + for uname, body, votes, created in rows: + db.session.add(Comment(discussion_id=disc.id, author_username=uname, + body=body, votes=votes, created_at=_dt(created))) + db.session.commit() + + +def seed_benchmark_data(): + """Give benchmark users pre-existing saved items, joined competitions, and + follows so their account pages feel populated. Gated as a whole for + idempotency. Deliberately avoids every tasks.jsonl target so the benchmark + tasks stay valid (e.g. alice does NOT pre-join 'llm-prompt-recovery').""" + alice = User.query.filter_by(email="alice.j@test.com").first() + if not alice: + return + if Bookmark.query.filter_by(user_id=alice.id).first(): + return # already seeded + + def bk(u, etype, slug, model): + ent = model.query.filter_by(slug=slug).first() + if u and ent: + db.session.add(Bookmark(user_id=u.id, entity_type=etype, entity_id=ent.id)) + + def join(u, slug, team): + c = Competition.query.filter_by(slug=slug).first() + if u and c: + db.session.add(CompetitionEntry(user_id=u.id, competition_id=c.id, + team_name=team, accepted_rules=True, + joined_at=mirror_now())) + + bob = User.query.filter_by(email="bob.c@test.com").first() + carol = User.query.filter_by(email="carol.d@test.com").first() + + bk(alice, "notebook", "co2-emissions-trends-eda", Notebook) + bk(alice, "dataset", "spotify-tracks-audio-features", Dataset) + join(alice, "titanic-survival", "alicejdata") + db.session.add(Follow(user_id=alice.id, target_username="datasmith_io")) + + bk(bob, "dataset", "student-performance-factors", Dataset) + join(bob, "handwritten-digit-recognizer", "bobsmith_ml") + + bk(carol, "model", "resnet50-chestxray", Model) + db.session.add(Follow(user_id=carol.id, target_username="kenji_cv")) + db.session.commit() + + +with app.app_context(): + db.create_all() + seed_benchmark_users() + seed_users() + seed_competitions() + seed_datasets() + seed_notebooks() + seed_models() + seed_courses() + seed_discussions() + seed_benchmark_data() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/kaggle/requirements.txt b/sites/kaggle/requirements.txt new file mode 100644 index 00000000..ede23c8f --- /dev/null +++ b/sites/kaggle/requirements.txt @@ -0,0 +1,7 @@ +Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +Flask-WTF==1.2.2 +Flask-Bcrypt==1.0.1 +WTForms==3.2.1 +email-validator==2.2.0 diff --git a/sites/kaggle/seed_data.py b/sites/kaggle/seed_data.py new file mode 100644 index 00000000..a2e7e09a --- /dev/null +++ b/sites/kaggle/seed_data.py @@ -0,0 +1,890 @@ +"""Kaggle mirror — seed catalog data. + +Plain Python data structures consumed by app.py's idempotent seed_*() functions. +No SQLAlchemy / Flask imports here so this module can be inspected standalone. + +Kaggle's domain entities: + User — competitors with progression tiers + performance medals + Competition — hosted ML challenges with prizes, leaderboards, deadlines + Dataset — community-uploaded datasets with usability scores + Notebook — public code (kernels), Python/R, optionally linked to a dataset + Model — pretrained models with framework + variations + Course — Kaggle Learn micro-courses + Discussion — forum threads across category forums + +Everything here is deterministic — no randomness at module import or seed time, +so the byte-identical-reset invariant holds. +""" + +# ---------------------------------------------------------------------------- +# Controlled vocabularies +# ---------------------------------------------------------------------------- +PERFORMANCE_TIERS = ["Novice", "Contributor", "Expert", "Master", "Grandmaster"] + +COMPETITION_CATEGORIES = [ + "Featured", "Research", "Getting Started", "Playground", + "Community", "Analytics", +] + +DISCUSSION_FORUMS = [ + "General", "Getting Started", "Product Feedback", + "Questions & Answers", "Competition Hosting", "Datasets", +] + +ML_FRAMEWORKS = ["PyTorch", "TensorFlow", "JAX", "Keras", "scikit-learn", "Transformers"] + +LICENSES = [ + "CC0: Public Domain", "CC BY-SA 4.0", "CC BY 4.0", "MIT", + "Apache 2.0", "GPL 2", "Other (specified in description)", + "Database: Open Database, Contents: Database Contents", +] + +PROGRAMMING_LANGUAGES = ["Python", "R"] + +# ---------------------------------------------------------------------------- +# Users — competitors. `tiers` maps the four ranked categories to a tier. +# medals are lifetime (gold, silver, bronze) performance medals. +# The first four (alice/bob/carol/david) are the standard WebHarbor benchmark +# accounts; the rest are notable-style Grandmasters for catalog depth. +# ---------------------------------------------------------------------------- +BENCHMARK_USERS = [ + { + "email": "alice.j@test.com", "username": "alicejdata", "password": "TestPass123!", + "display_name": "Alice Johnson", "tier": "Expert", + "tiers": {"competitions": "Expert", "datasets": "Master", "notebooks": "Expert", "discussions": "Contributor"}, + "points": 48210, "gold": 1, "silver": 6, "bronze": 14, + "location": "Seattle, United States", "occupation": "Data Scientist", "organization": "Cascadia Analytics", + "bio": "ML practitioner focused on tabular modeling and feature engineering. Datasets Master.", + "joined": "2019-03-11", "avatar": "alicejdata", + }, + { + "email": "bob.c@test.com", "username": "bobsmith_ml", "password": "TestPass123!", + "display_name": "Bob Smith", "tier": "Contributor", + "tiers": {"competitions": "Contributor", "datasets": "Contributor", "notebooks": "Novice", "discussions": "Novice"}, + "points": 9120, "gold": 0, "silver": 1, "bronze": 5, + "location": "Manchester, United Kingdom", "occupation": "Student", "organization": "University of Manchester", + "bio": "Learning the ropes — mostly Getting Started competitions and Kaggle Learn.", + "joined": "2022-09-02", "avatar": "bobsmith_ml", + }, + { + "email": "carol.d@test.com", "username": "carolwong", "password": "TestPass123!", + "display_name": "Carol Wong", "tier": "Master", + "tiers": {"competitions": "Master", "datasets": "Expert", "notebooks": "Master", "discussions": "Expert"}, + "points": 102450, "gold": 4, "silver": 11, "bronze": 9, + "location": "Singapore", "occupation": "Research Engineer", "organization": "DeepReef AI", + "bio": "Computer vision and deep learning. Two-time competition gold medalist.", + "joined": "2017-06-20", "avatar": "carolwong", + }, + { + "email": "david.k@test.com", "username": "davidtran", "password": "TestPass123!", + "display_name": "David Tran", "tier": "Novice", + "tiers": {"competitions": "Novice", "datasets": "Novice", "notebooks": "Novice", "discussions": "Novice"}, + "points": 320, "gold": 0, "silver": 0, "bronze": 0, + "location": "Toronto, Canada", "occupation": "Software Engineer", "organization": "", + "bio": "Just joined Kaggle to learn data science.", + "joined": "2026-05-30", "avatar": "davidtran", + }, +] + +NOTABLE_USERS = [ + { + "username": "psi_grandmaster", "display_name": "Priya Sharma", "tier": "Grandmaster", + "tiers": {"competitions": "Grandmaster", "datasets": "Master", "notebooks": "Grandmaster", "discussions": "Master"}, + "points": 312400, "gold": 19, "silver": 22, "bronze": 17, "comp_rank": 3, + "location": "Bengaluru, India", "occupation": "Principal Data Scientist", "organization": "Helios Labs", + "bio": "Competitions Grandmaster. Gradient boosting, stacking, and relentless cross-validation.", + "joined": "2014-08-01", "avatar": "psi_grandmaster", + }, + { + "username": "kenji_cv", "display_name": "Kenji Watanabe", "tier": "Grandmaster", + "tiers": {"competitions": "Grandmaster", "datasets": "Expert", "notebooks": "Master", "discussions": "Expert"}, + "points": 287600, "gold": 16, "silver": 25, "bronze": 12, "comp_rank": 7, + "location": "Tokyo, Japan", "occupation": "Senior ML Engineer", "organization": "Sakura Vision", + "bio": "Image segmentation and medical imaging specialist.", + "joined": "2015-02-14", "avatar": "kenji_cv", + }, + { + "username": "datasmith_io", "display_name": "Lena Fischer", "tier": "Grandmaster", + "tiers": {"competitions": "Master", "datasets": "Grandmaster", "notebooks": "Grandmaster", "discussions": "Grandmaster"}, + "points": 198750, "gold": 6, "silver": 14, "bronze": 31, "comp_rank": 41, + "location": "Berlin, Germany", "occupation": "Data Engineer", "organization": "Open Data Collective", + "bio": "Datasets & Notebooks Grandmaster. I publish clean, well-documented public datasets.", + "joined": "2016-11-30", "avatar": "datasmith_io", + }, + { + "username": "marco_nlp", "display_name": "Marco Rossi", "tier": "Master", + "tiers": {"competitions": "Master", "datasets": "Expert", "notebooks": "Master", "discussions": "Master"}, + "points": 121300, "gold": 3, "silver": 9, "bronze": 18, "comp_rank": 96, + "location": "Milan, Italy", "occupation": "NLP Researcher", "organization": "Lingua Systems", + "bio": "Natural language processing, transformers, and LLM fine-tuning.", + "joined": "2018-04-22", "avatar": "marco_nlp", + }, + { + "username": "sara_timeseries", "display_name": "Sara Okafor", "tier": "Master", + "tiers": {"competitions": "Master", "datasets": "Master", "notebooks": "Expert", "discussions": "Contributor"}, + "points": 88900, "gold": 2, "silver": 7, "bronze": 13, "comp_rank": 158, + "location": "Lagos, Nigeria", "occupation": "Quantitative Analyst", "organization": "Sahel Capital", + "bio": "Time-series forecasting and demand prediction.", + "joined": "2018-12-05", "avatar": "sara_timeseries", + }, + { + "username": "tomeka_viz", "display_name": "Tomeka Banks", "tier": "Expert", + "tiers": {"competitions": "Contributor", "datasets": "Expert", "notebooks": "Grandmaster", "discussions": "Expert"}, + "points": 76400, "gold": 1, "silver": 4, "bronze": 22, "comp_rank": 402, + "location": "Atlanta, United States", "occupation": "Data Visualization Lead", "organization": "ClearChart", + "bio": "Notebooks Grandmaster. I make EDA notebooks people actually read.", + "joined": "2019-07-18", "avatar": "tomeka_viz", + }, + { + "username": "raul_gbm", "display_name": "Raúl Mendoza", "tier": "Grandmaster", + "tiers": {"competitions": "Grandmaster", "datasets": "Contributor", "notebooks": "Expert", "discussions": "Master"}, + "points": 256100, "gold": 12, "silver": 19, "bronze": 21, "comp_rank": 12, + "location": "Mexico City, Mexico", "occupation": "Kaggle Competitions Grandmaster", "organization": "", + "bio": "Full-time competitor. LightGBM, XGBoost, and a lot of coffee.", + "joined": "2015-09-09", "avatar": "raul_gbm", + }, + { + "username": "hosting_org_zindi", "display_name": "Global Health Data Initiative", "tier": "Contributor", + "tiers": {"competitions": "Contributor", "datasets": "Master", "notebooks": "Novice", "discussions": "Contributor"}, + "points": 14200, "gold": 0, "silver": 2, "bronze": 6, "comp_rank": None, + "location": "Geneva, Switzerland", "occupation": "Competition Host", "organization": "GHDI", + "bio": "Non-profit hosting public-health ML challenges.", + "joined": "2020-01-15", "avatar": "hosting_org_zindi", "is_org": True, + }, +] + +# ---------------------------------------------------------------------------- +# Competitions +# Each: slug, title, subtitle, category, host (org name), reward (display), +# reward_value (numeric USD for sorting; 0 for non-cash), metric, num_teams, +# deadline (ISO), tags, thumbnail key, description, owner_username (host acct). +# active=True means deadline in the future relative to the mirror clock. +# ---------------------------------------------------------------------------- +COMPETITIONS = [ + { + "slug": "spaceship-titanic-rescue", + "title": "Spaceship Titanic: Predict the Rescued", + "subtitle": "Predict which passengers were transported to an alternate dimension", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "Classification Accuracy", "num_teams": 2841, "deadline": "2027-01-01", + "tags": ["binary classification", "tabular", "beginner"], "thumbnail": "spaceship-titanic-rescue", + "owner": "Kaggle", + "description": "A beginner-friendly classification challenge. Given passenger records from the interstellar liner Spaceship Titanic, predict whether each passenger was transported to an alternate dimension during the disaster. Perfect for learning feature engineering and model validation.", + }, + { + "slug": "house-prices-advanced-regression", + "title": "House Prices: Advanced Regression Techniques", + "subtitle": "Predict final sale prices of residential homes in Ames, Iowa", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "RMSE (log scale)", "num_teams": 4602, "deadline": "2027-01-01", + "tags": ["regression", "tabular", "feature engineering"], "thumbnail": "house-prices-advanced-regression", + "owner": "Kaggle", + "description": "With 79 explanatory variables describing almost every aspect of residential homes, predict the final sale price. A classic regression competition that rewards careful feature engineering and ensembling.", + }, + { + "slug": "rsna-pneumonia-detection-2026", + "title": "RSNA Pneumonia Detection Challenge 2026", + "subtitle": "Detect pneumonia in chest radiographs", + "category": "Featured", "host": "Radiological Society of North America", "reward": "$30,000", "reward_value": 30000, + "metric": "Mean Average Precision (mAP)", "num_teams": 1187, "deadline": "2026-09-15", + "tags": ["computer vision", "medical imaging", "object detection"], "thumbnail": "rsna-pneumonia-detection-2026", + "owner": "kenji_cv", + "description": "Build an algorithm to detect a visual signal for pneumonia in medical images. Each image may contain zero, one, or several bounding-box regions of opacity. Evaluation is mean average precision at multiple IoU thresholds.", + }, + { + "slug": "global-wheat-yield-forecast", + "title": "Global Wheat Yield Forecast", + "subtitle": "Forecast regional wheat yields from satellite and weather data", + "category": "Research", "host": "FAO Agricultural Data Lab", "reward": "$75,000", "reward_value": 75000, + "metric": "Root Mean Squared Error", "num_teams": 624, "deadline": "2026-08-01", + "tags": ["regression", "time series", "geospatial", "climate"], "thumbnail": "global-wheat-yield-forecast", + "owner": "sara_timeseries", + "description": "Forecast end-of-season wheat yields for agricultural regions worldwide using multi-temporal satellite imagery, soil records, and weather station data. A research competition aimed at improving food-security planning.", + }, + { + "slug": "llm-prompt-recovery", + "title": "LLM Prompt Recovery", + "subtitle": "Recover the prompt used to transform a piece of text", + "category": "Featured", "host": "Kaggle", "reward": "$50,000", "reward_value": 50000, + "metric": "Mean Sharpened Cosine Similarity", "num_teams": 2210, "deadline": "2026-07-10", + "tags": ["nlp", "llm", "text"], "thumbnail": "llm-prompt-recovery", + "owner": "marco_nlp", + "description": "Given an original text and its rewritten version, recover the natural-language instruction (prompt) that an LLM was given to perform the rewrite. Submissions are scored by sharpened cosine similarity between predicted and ground-truth prompt embeddings.", + }, + { + "slug": "store-sales-demand-forecasting", + "title": "Store Sales — Time Series Forecasting", + "subtitle": "Forecast grocery sales for thousands of product families", + "category": "Playground", "host": "Kaggle", "reward": "Swag", "reward_value": 0, + "metric": "Root Mean Squared Logarithmic Error", "num_teams": 1893, "deadline": "2026-12-31", + "tags": ["time series", "regression", "retail"], "thumbnail": "store-sales-demand-forecasting", + "owner": "Kaggle", + "description": "Predict unit sales for thousands of items sold at different stores. Practice time-series feature engineering with promotions, holidays, and oil-price covariates. Monthly Playground-style challenge.", + }, + { + "slug": "malaria-cell-classification", + "title": "Malaria Cell Image Classification", + "subtitle": "Classify blood-smear cell images as infected or healthy", + "category": "Featured", "host": "Global Health Data Initiative", "reward": "$25,000", "reward_value": 25000, + "metric": "ROC AUC", "num_teams": 944, "deadline": "2026-06-30", + "tags": ["computer vision", "medical imaging", "binary classification", "health"], "thumbnail": "malaria-cell-classification", + "owner": "hosting_org_zindi", + "description": "Classify segmented red-blood-cell images from thin blood smears as parasitized or uninfected. Hosted by a non-profit to accelerate low-cost malaria screening in the field.", + }, + { + "slug": "nyc-taxi-fare-prediction", + "title": "New York City Taxi Fare Prediction", + "subtitle": "Predict the fare of a NYC taxi ride", + "category": "Playground", "host": "Kaggle", "reward": "Swag", "reward_value": 0, + "metric": "Root Mean Squared Error", "num_teams": 1402, "deadline": "2026-11-20", + "tags": ["regression", "tabular", "geospatial"], "thumbnail": "nyc-taxi-fare-prediction", + "owner": "Kaggle", + "description": "Predict taxi fare amounts in New York City from pickup/dropoff coordinates, timestamps, and passenger counts. A great playground for geospatial feature engineering.", + }, + { + "slug": "credit-default-risk-2026", + "title": "Home Credit Default Risk 2026", + "subtitle": "Predict how capable each applicant is of repaying a loan", + "category": "Featured", "host": "Home Credit Group", "reward": "$100,000", "reward_value": 100000, + "metric": "ROC AUC", "num_teams": 3308, "deadline": "2026-10-05", + "tags": ["tabular", "binary classification", "finance"], "thumbnail": "credit-default-risk-2026", + "owner": "psi_grandmaster", + "description": "Use historical application, credit-bureau, and installment data to predict loan-repayment ability for applicants with little or no credit history. The largest cash prize on the platform this season.", + }, + { + "slug": "leaf-disease-segmentation", + "title": "Plant Leaf Disease Segmentation", + "subtitle": "Segment diseased regions on crop-leaf photographs", + "category": "Research", "host": "FAO Agricultural Data Lab", "reward": "$40,000", "reward_value": 40000, + "metric": "Dice Coefficient", "num_teams": 511, "deadline": "2026-09-28", + "tags": ["computer vision", "segmentation", "agriculture"], "thumbnail": "leaf-disease-segmentation", + "owner": "kenji_cv", + "description": "Pixel-level segmentation of disease lesions on leaf images across 12 crop species. Helps build smartphone tools for early disease detection by smallholder farmers.", + }, + { + "slug": "titanic-survival", + "title": "Titanic — Machine Learning from Disaster", + "subtitle": "The legendary starter competition: predict survival on the Titanic", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "Classification Accuracy", "num_teams": 14820, "deadline": "2027-01-01", + "tags": ["binary classification", "tabular", "beginner"], "thumbnail": "titanic-survival", + "owner": "Kaggle", + "description": "The most popular Getting Started competition. Use passenger data (name, age, sex, class, fare) to predict who survived the 1912 Titanic shipwreck. Your first stop on Kaggle.", + }, + { + "slug": "retail-customer-churn-analytics", + "title": "Retail Customer Churn Analytics", + "subtitle": "Analytics challenge: explain and predict subscriber churn", + "category": "Analytics", "host": "Streamline Retail", "reward": "$15,000", "reward_value": 15000, + "metric": "Judged (report quality)", "num_teams": 287, "deadline": "2026-07-31", + "tags": ["analytics", "tabular", "business"], "thumbnail": "retail-customer-churn-analytics", + "owner": "tomeka_viz", + "description": "An analytics competition judged on the quality of your written analysis and visualizations, not a leaderboard metric. Identify the key drivers of customer churn and recommend retention strategies.", + }, + { + "slug": "arctic-sea-ice-forecast", + "title": "Arctic Sea Ice Extent Forecast", + "subtitle": "Forecast monthly Arctic sea-ice extent", + "category": "Research", "host": "Polar Climate Consortium", "reward": "$60,000", "reward_value": 60000, + "metric": "Mean Absolute Error", "num_teams": 398, "deadline": "2026-08-20", + "tags": ["time series", "climate", "regression", "geospatial"], "thumbnail": "arctic-sea-ice-forecast", + "owner": "sara_timeseries", + "description": "Forecast pan-Arctic and regional sea-ice extent up to six months ahead from satellite passive-microwave records and reanalysis climate fields. A research competition supporting climate science.", + }, + { + "slug": "handwritten-digit-recognizer", + "title": "Digit Recognizer", + "subtitle": "Learn computer vision fundamentals with the famous MNIST data", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "Classification Accuracy", "num_teams": 5210, "deadline": "2027-01-01", + "tags": ["computer vision", "image classification", "beginner"], "thumbnail": "handwritten-digit-recognizer", + "owner": "Kaggle", + "description": "Identify handwritten digits 0–9 from 28×28 grayscale images (MNIST). The standard introduction to image classification on Kaggle.", + }, + { + "slug": "fraud-detection-stream", + "title": "Real-Time Fraud Detection", + "subtitle": "Flag fraudulent transactions in a streaming feed", + "category": "Featured", "host": "PayServe", "reward": "$80,000", "reward_value": 80000, + "metric": "PR AUC", "num_teams": 1605, "deadline": "2026-06-26", + "tags": ["tabular", "binary classification", "finance", "imbalanced"], "thumbnail": "fraud-detection-stream", + "owner": "raul_gbm", + "description": "Detect fraudulent card transactions in a highly imbalanced stream where fewer than 0.2% of records are fraud. Scored by area under the precision-recall curve. Closes soon.", + }, + { + "slug": "sentiment-of-product-reviews", + "title": "Sentiment of Product Reviews", + "subtitle": "Playground NLP: classify review sentiment into five stars", + "category": "Playground", "host": "Kaggle", "reward": "Swag", "reward_value": 0, + "metric": "Quadratic Weighted Kappa", "num_teams": 1021, "deadline": "2026-12-15", + "tags": ["nlp", "text", "ordinal classification"], "thumbnail": "sentiment-of-product-reviews", + "owner": "marco_nlp", + "description": "Predict the 1–5 star rating implied by the text of an online product review. A monthly Playground competition for practicing text classification and ordinal targets.", + }, + { + "slug": "energy-load-forecasting", + "title": "City Energy Load Forecasting", + "subtitle": "Forecast hourly electricity demand for a metropolitan grid", + "category": "Research", "host": "GridSense Energy", "reward": "$45,000", "reward_value": 45000, + "metric": "Mean Absolute Percentage Error", "num_teams": 472, "deadline": "2026-10-30", + "tags": ["time series", "regression", "energy"], "thumbnail": "energy-load-forecasting", + "owner": "sara_timeseries", + "description": "Forecast hourly electricity load for a large city grid using weather forecasts, calendar features, and historical consumption. Helps utilities plan generation and reduce waste.", + }, +] + +# Leaderboard submissions per competition slug. Each tuple: +# (rank, team_name, owner_username, score, submitted_at). Scores ordered by rank. +LEADERBOARDS = { + "credit-default-risk-2026": [ + (1, "Gradient Surfers", "psi_grandmaster", 0.81342, "2026-06-18"), + (2, "Boosted Beavers", "raul_gbm", 0.81197, "2026-06-19"), + (3, "Reef Net", "carolwong", 0.80955, "2026-06-15"), + (4, "Data Mavens", "datasmith_io", 0.80610, "2026-06-17"), + (5, "alicejdata", "alicejdata", 0.80288, "2026-06-16"), + (6, "Mendoza Solo", "marco_nlp", 0.79944, "2026-06-12"), + ], + "fraud-detection-stream": [ + (1, "Boosted Beavers", "raul_gbm", 0.91205, "2026-06-21"), + (2, "Gradient Surfers", "psi_grandmaster", 0.90880, "2026-06-20"), + (3, "Sahel Signals", "sara_timeseries", 0.89712, "2026-06-19"), + (4, "alicejdata", "alicejdata", 0.88450, "2026-06-18"), + ], + "rsna-pneumonia-detection-2026": [ + (1, "Sakura Vision", "kenji_cv", 0.26412, "2026-06-10"), + (2, "Reef Net", "carolwong", 0.25988, "2026-06-14"), + (3, "PixelMedics", "tomeka_viz", 0.24501, "2026-06-09"), + ], + "llm-prompt-recovery": [ + (1, "Lingua Systems", "marco_nlp", 0.74129, "2026-06-20"), + (2, "Gradient Surfers", "psi_grandmaster", 0.73004, "2026-06-19"), + (3, "carolwong", "carolwong", 0.71880, "2026-06-17"), + ], +} + +# ---------------------------------------------------------------------------- +# Datasets +# slug, title, subtitle, owner_username, size (display), size_bytes (sort), +# file_count, file_types, usability (0-10 one decimal), upvotes, downloads, +# views, license, tags, last_updated (ISO), thumbnail, description +# ---------------------------------------------------------------------------- +DATASETS = [ + { + "slug": "global-co2-emissions-1960-2025", "owner": "datasmith_io", + "title": "Global CO₂ Emissions 1960–2025", + "subtitle": "Per-country annual CO₂ emissions, GDP, and population", + "size": "44 MB", "size_bytes": 46137344, "file_count": 3, "file_types": "CSV", + "usability": 10.0, "upvotes": 1842, "downloads": 39120, "views": 210400, + "license": "CC0: Public Domain", "tags": ["climate", "economics", "environment", "tabular"], + "last_updated": "2026-05-28", "thumbnail": "global-co2-emissions-1960-2025", + "description": "A tidy, country-level panel of annual CO₂ emissions (total and per-capita) joined with GDP and population from 1960 to 2025. Cleaned, de-duplicated, and ready for analysis.", + }, + { + "slug": "imdb-50k-movie-reviews", "owner": "marco_nlp", + "title": "IMDB 50K Movie Reviews", + "subtitle": "Balanced binary sentiment classification corpus", + "size": "66 MB", "size_bytes": 69206016, "file_count": 1, "file_types": "CSV", + "usability": 9.4, "upvotes": 3201, "downloads": 88210, "views": 402100, + "license": "Other (specified in description)", "tags": ["nlp", "text", "sentiment", "binary classification"], + "last_updated": "2025-11-12", "thumbnail": "imdb-50k-movie-reviews", + "description": "50,000 highly polar movie reviews labeled positive or negative, split 25k/25k for training and testing. The standard benchmark for binary sentiment classification.", + }, + { + "slug": "nyc-airbnb-2026", "owner": "alicejdata", + "title": "New York City Airbnb Listings 2026", + "subtitle": "Listing prices, locations, availability, and host details", + "size": "18 MB", "size_bytes": 18874368, "file_count": 2, "file_types": "CSV, GeoJSON", + "usability": 9.1, "upvotes": 1204, "downloads": 28740, "views": 150300, + "license": "CC BY-SA 4.0", "tags": ["geospatial", "tabular", "tourism", "pricing"], + "last_updated": "2026-04-02", "thumbnail": "nyc-airbnb-2026", + "description": "Every active Airbnb listing in NYC's five boroughs as of Q1 2026, with nightly price, room type, neighborhood, review counts, and host data. Includes a GeoJSON of neighborhood boundaries.", + }, + { + "slug": "chest-xray-pneumonia", "owner": "kenji_cv", + "title": "Chest X-Ray Images (Pneumonia)", + "subtitle": "5,863 labeled pediatric chest radiographs", + "size": "1.2 GB", "size_bytes": 1288490188, "file_count": 5863, "file_types": "JPEG", + "usability": 8.8, "upvotes": 5420, "downloads": 142300, "views": 610200, + "license": "CC BY 4.0", "tags": ["computer vision", "medical imaging", "health", "image classification"], + "last_updated": "2025-09-30", "thumbnail": "chest-xray-pneumonia", + "description": "Anterior-posterior chest X-ray images of pediatric patients, organized into NORMAL and PNEUMONIA folders for train/val/test. Widely used to benchmark medical-image classifiers.", + }, + { + "slug": "world-happiness-report-2026", "owner": "datasmith_io", + "title": "World Happiness Report 2026", + "subtitle": "National happiness scores and their six explanatory factors", + "size": "2 MB", "size_bytes": 2097152, "file_count": 1, "file_types": "CSV", + "usability": 10.0, "upvotes": 2987, "downloads": 61200, "views": 288700, + "license": "CC0: Public Domain", "tags": ["economics", "social science", "tabular", "survey"], + "last_updated": "2026-03-20", "thumbnail": "world-happiness-report-2026", + "description": "Ladder-of-life happiness scores for 150+ countries with the six contributing factors (GDP per capita, social support, healthy life expectancy, freedom, generosity, perceptions of corruption).", + }, + { + "slug": "credit-card-fraud-transactions", "owner": "raul_gbm", + "title": "Credit Card Fraud Transactions", + "subtitle": "Anonymized European card transactions, highly imbalanced", + "size": "150 MB", "size_bytes": 157286400, "file_count": 1, "file_types": "CSV", + "usability": 9.7, "upvotes": 8810, "downloads": 233400, "views": 901200, + "license": "Database: Open Database, Contents: Database Contents", "tags": ["finance", "tabular", "binary classification", "imbalanced"], + "last_updated": "2025-12-01", "thumbnail": "credit-card-fraud-transactions", + "description": "284,807 card transactions over two days, 492 of them fraudulent (0.172%). Features are PCA-transformed for privacy except Time and Amount. The canonical imbalanced-classification dataset.", + }, + { + "slug": "spotify-tracks-audio-features", "owner": "tomeka_viz", + "title": "Spotify Tracks — Audio Features", + "subtitle": "114k tracks with danceability, energy, valence, and more", + "size": "20 MB", "size_bytes": 20971520, "file_count": 1, "file_types": "CSV", + "usability": 9.4, "upvotes": 2110, "downloads": 47800, "views": 199600, + "license": "CC0: Public Domain", "tags": ["music", "tabular", "audio", "eda"], + "last_updated": "2026-01-18", "thumbnail": "spotify-tracks-audio-features", + "description": "114,000 Spotify tracks across 125 genres with audio features (tempo, energy, danceability, valence, acousticness) and popularity. Great for EDA, clustering, and recommendation.", + }, + { + "slug": "global-wheat-satellite-imagery", "owner": "sara_timeseries", + "title": "Global Wheat Satellite Imagery", + "subtitle": "Multi-temporal Sentinel-2 patches over wheat-growing regions", + "size": "3.4 GB", "size_bytes": 3650722201, "file_count": 21840, "file_types": "GeoTIFF, CSV", + "usability": 8.5, "upvotes": 742, "downloads": 9120, "views": 51200, + "license": "CC BY 4.0", "tags": ["geospatial", "computer vision", "agriculture", "climate", "time series"], + "last_updated": "2026-05-10", "thumbnail": "global-wheat-satellite-imagery", + "description": "Cloud-free Sentinel-2 image patches sampled across global wheat belts at five growth stages, paired with end-of-season yield labels. The companion dataset to the Global Wheat Yield Forecast competition.", + }, + { + "slug": "us-used-car-listings-2026", "owner": "alicejdata", + "title": "US Used Car Listings 2026", + "subtitle": "400k listings with price, mileage, make, model, and condition", + "size": "92 MB", "size_bytes": 96468992, "file_count": 1, "file_types": "CSV", + "usability": 9.2, "upvotes": 1533, "downloads": 35600, "views": 162900, + "license": "CC BY-SA 4.0", "tags": ["tabular", "pricing", "regression", "automotive"], + "last_updated": "2026-04-25", "thumbnail": "us-used-car-listings-2026", + "description": "Roughly 400,000 used-car listings scraped from US dealer sites in early 2026, with asking price, odometer, year, make, model, trim, fuel type, and condition. A solid regression playground.", + }, + { + "slug": "handwritten-digits-mnist", "owner": "Kaggle", + "title": "MNIST Handwritten Digits", + "subtitle": "70,000 labeled 28×28 grayscale digit images", + "size": "11 MB", "size_bytes": 11534336, "file_count": 4, "file_types": "CSV, IDX", + "usability": 10.0, "upvotes": 4290, "downloads": 178200, "views": 720100, + "license": "CC0: Public Domain", "tags": ["computer vision", "image classification", "beginner"], + "last_updated": "2025-08-14", "thumbnail": "handwritten-digits-mnist", + "description": "The classic MNIST dataset of handwritten digits 0–9 as flattened pixel CSVs and original IDX files. The 'hello world' of computer vision.", + }, + { + "slug": "amazon-product-reviews-electronics", "owner": "marco_nlp", + "title": "Amazon Product Reviews — Electronics", + "subtitle": "1.2M reviews with star ratings and helpfulness votes", + "size": "480 MB", "size_bytes": 503316480, "file_count": 1, "file_types": "JSON", + "usability": 8.9, "upvotes": 2670, "downloads": 58900, "views": 244300, + "license": "Other (specified in description)", "tags": ["nlp", "text", "sentiment", "ecommerce"], + "last_updated": "2025-10-22", "thumbnail": "amazon-product-reviews-electronics", + "description": "1.2 million electronics-category product reviews with 1–5 star ratings, review text, summary, and helpfulness votes. Use for sentiment, ordinal regression, or recommendation.", + }, + { + "slug": "world-cities-population", "owner": "datasmith_io", + "title": "World Cities Population & Coordinates", + "subtitle": "47k cities with population, country, and lat/long", + "size": "6 MB", "size_bytes": 6291456, "file_count": 1, "file_types": "CSV", + "usability": 9.8, "upvotes": 1920, "downloads": 51000, "views": 233100, + "license": "CC BY 4.0", "tags": ["geospatial", "tabular", "reference"], + "last_updated": "2026-02-09", "thumbnail": "world-cities-population", + "description": "47,000 world cities with population estimates, country and admin region, time zone, and latitude/longitude. A handy reference table for joining and mapping.", + }, + { + "slug": "retail-store-sales-history", "owner": "sara_timeseries", + "title": "Retail Store Sales History", + "subtitle": "Five years of daily sales across 54 stores and 33 families", + "size": "120 MB", "size_bytes": 125829120, "file_count": 6, "file_types": "CSV", + "usability": 9.3, "upvotes": 1340, "downloads": 31200, "views": 142800, + "license": "CC0: Public Domain", "tags": ["time series", "retail", "regression", "tabular"], + "last_updated": "2026-05-15", "thumbnail": "retail-store-sales-history", + "description": "Daily unit sales for 54 stores and 33 product families over five years, with promotions, holidays, oil prices, and store metadata. Companion data for the Store Sales forecasting competition.", + }, + { + "slug": "plant-leaf-disease-images", "owner": "kenji_cv", + "title": "Plant Leaf Disease Images", + "subtitle": "54k annotated leaf photos across 12 crop species", + "size": "2.1 GB", "size_bytes": 2254857830, "file_count": 54306, "file_types": "JPEG, PNG masks", + "usability": 8.7, "upvotes": 1108, "downloads": 17400, "views": 78200, + "license": "CC BY-SA 4.0", "tags": ["computer vision", "segmentation", "agriculture", "image classification"], + "last_updated": "2026-05-05", "thumbnail": "plant-leaf-disease-images", + "description": "54,306 leaf images spanning healthy and 26 disease classes across 12 crops, each with a pixel mask of the diseased region. Companion to the Plant Leaf Disease Segmentation competition.", + }, + { + "slug": "global-temperature-anomalies", "owner": "datasmith_io", + "title": "Global Temperature Anomalies 1880–2025", + "subtitle": "Monthly land+ocean temperature anomalies", + "size": "4 MB", "size_bytes": 4194304, "file_count": 2, "file_types": "CSV", + "usability": 9.9, "upvotes": 2240, "downloads": 49800, "views": 211000, + "license": "CC0: Public Domain", "tags": ["climate", "time series", "environment", "tabular"], + "last_updated": "2026-03-01", "thumbnail": "global-temperature-anomalies", + "description": "Monthly global land and ocean temperature anomalies relative to the 1951–1980 baseline, 1880 to present. Tidy and ready for time-series and trend analysis.", + }, + { + "slug": "student-performance-factors", "owner": "bobsmith_ml", + "title": "Student Performance Factors", + "subtitle": "Exam scores with study habits and background features", + "size": "1 MB", "size_bytes": 1048576, "file_count": 1, "file_types": "CSV", + "usability": 9.0, "upvotes": 980, "downloads": 22100, "views": 96500, + "license": "CC BY 4.0", "tags": ["education", "tabular", "regression", "beginner"], + "last_updated": "2026-06-01", "thumbnail": "student-performance-factors", + "description": "Synthetic-but-realistic records of student exam scores with study hours, attendance, parental involvement, sleep, and tutoring. A clean beginner regression dataset.", + }, +] + +# ---------------------------------------------------------------------------- +# Notebooks (Kernels / Code) +# slug, title, author_username, language, votes, comments, medal (or None), +# best_score (display or None), runtime, last_run (ISO), linked_competition, +# linked_dataset, thumbnail, description, tags +# ---------------------------------------------------------------------------- +NOTEBOOKS = [ + { + "slug": "eda-credit-default-deep-dive", "author": "psi_grandmaster", + "title": "Home Credit — Full EDA & Feature Factory", + "language": "Python", "votes": 1842, "comments": 214, "medal": "gold", + "best_score": None, "runtime": "428.6s", "last_run": "2026-06-15", + "linked_competition": "credit-default-risk-2026", "linked_dataset": None, + "thumbnail": "eda-credit-default-deep-dive", "tags": ["eda", "feature engineering", "lightgbm", "tabular"], + "description": "An end-to-end exploratory analysis of the Home Credit Default Risk data plus a reusable feature-engineering pipeline that joins all auxiliary tables and produces 700+ aggregated features.", + }, + { + "slug": "lgbm-baseline-fraud", "author": "raul_gbm", + "title": "LightGBM Baseline for Real-Time Fraud Detection", + "language": "Python", "votes": 1204, "comments": 138, "medal": "gold", + "best_score": "0.91205", "runtime": "92.4s", "last_run": "2026-06-21", + "linked_competition": "fraud-detection-stream", "linked_dataset": "credit-card-fraud-transactions", + "thumbnail": "lgbm-baseline-fraud", "tags": ["lightgbm", "imbalanced", "tabular", "baseline"], + "description": "A clean, well-commented LightGBM baseline for the Real-Time Fraud Detection competition, with class-weighting, threshold tuning on the PR curve, and out-of-fold validation. Reproduces a 0.912 leaderboard score.", + }, + { + "slug": "unet-pneumonia-segmentation", "author": "kenji_cv", + "title": "U-Net Pneumonia Detection — Training Pipeline", + "language": "Python", "votes": 932, "comments": 96, "medal": "gold", + "best_score": "0.264", "runtime": "3h 12m", "last_run": "2026-06-10", + "linked_competition": "rsna-pneumonia-detection-2026", "linked_dataset": "chest-xray-pneumonia", + "thumbnail": "unet-pneumonia-segmentation", "tags": ["pytorch", "computer vision", "segmentation", "medical imaging"], + "description": "A PyTorch U-Net with an EfficientNet encoder for the RSNA Pneumonia Detection Challenge, including augmentation, mixed-precision training, and a mAP evaluation harness.", + }, + { + "slug": "titanic-top-3-percent", "author": "carolwong", + "title": "Titanic — Top 3% Solution Walkthrough", + "language": "Python", "votes": 2610, "comments": 311, "medal": "gold", + "best_score": "0.81100", "runtime": "44.1s", "last_run": "2026-05-22", + "linked_competition": "titanic-survival", "linked_dataset": None, + "thumbnail": "titanic-top-3-percent", "tags": ["beginner", "feature engineering", "ensemble", "tabular"], + "description": "A friendly, fully explained Titanic solution that reaches the top 3% of the leaderboard with thoughtful feature engineering (titles, family size, fare bins) and a soft-voting ensemble.", + }, + { + "slug": "spotify-genre-clustering", "author": "tomeka_viz", + "title": "Spotify Audio Features — Clustering & Viz", + "language": "Python", "votes": 884, "comments": 73, "medal": "silver", + "best_score": None, "runtime": "61.2s", "last_run": "2026-02-01", + "linked_competition": None, "linked_dataset": "spotify-tracks-audio-features", + "thumbnail": "spotify-genre-clustering", "tags": ["eda", "clustering", "visualization", "music"], + "description": "Interactive UMAP + K-Means clustering of 114k Spotify tracks by audio features, with beautiful Plotly charts that reveal how genres separate in feature space.", + }, + { + "slug": "imdb-sentiment-transformers", "author": "marco_nlp", + "title": "IMDB Sentiment with DistilBERT (94% acc)", + "language": "Python", "votes": 1410, "comments": 152, "medal": "gold", + "best_score": "0.9412", "runtime": "27m 8s", "last_run": "2025-11-15", + "linked_competition": None, "linked_dataset": "imdb-50k-movie-reviews", + "thumbnail": "imdb-sentiment-transformers", "tags": ["nlp", "transformers", "pytorch", "sentiment"], + "description": "Fine-tunes DistilBERT on the IMDB 50K reviews dataset to 94% test accuracy, with a clean Hugging Face Trainer loop, learning-rate schedule, and confusion-matrix analysis.", + }, + { + "slug": "house-prices-stacked-regression", "author": "alicejdata", + "title": "House Prices — Stacked Regression (Top 5%)", + "language": "Python", "votes": 1190, "comments": 134, "medal": "silver", + "best_score": "0.11892", "runtime": "118.7s", "last_run": "2026-04-30", + "linked_competition": "house-prices-advanced-regression", "linked_dataset": None, + "thumbnail": "house-prices-stacked-regression", "tags": ["regression", "stacking", "feature engineering", "tabular"], + "description": "A stacked ensemble (Lasso, ElasticNet, Gradient Boosting, XGBoost) with careful skew correction and target encoding that lands in the top 5% of the House Prices leaderboard.", + }, + { + "slug": "r-timeseries-store-sales", "author": "sara_timeseries", + "title": "Store Sales Forecasting in R (fable + tsibble)", + "language": "R", "votes": 612, "comments": 58, "medal": "silver", + "best_score": "0.41203", "runtime": "204.9s", "last_run": "2026-05-18", + "linked_competition": "store-sales-demand-forecasting", "linked_dataset": "retail-store-sales-history", + "thumbnail": "r-timeseries-store-sales", "tags": ["r", "time series", "forecasting", "tidyverse"], + "description": "A tidyverts (fable + tsibble) workflow for the Store Sales competition in R: seasonal decomposition, exponential smoothing, and an ensemble of ETS and ARIMA per product family.", + }, + { + "slug": "mnist-cnn-from-scratch", "author": "bobsmith_ml", + "title": "MNIST CNN from Scratch (99.2% acc)", + "language": "Python", "votes": 421, "comments": 39, "medal": "bronze", + "best_score": "0.99214", "runtime": "12m 3s", "last_run": "2026-06-03", + "linked_competition": "handwritten-digit-recognizer", "linked_dataset": "handwritten-digits-mnist", + "thumbnail": "mnist-cnn-from-scratch", "tags": ["computer vision", "keras", "beginner", "cnn"], + "description": "A beginner-friendly Keras CNN for MNIST that reaches 99.2% with data augmentation and dropout, explained layer by layer.", + }, + { + "slug": "co2-emissions-trends-eda", "author": "datasmith_io", + "title": "Global CO₂ Emissions — Trends & Storytelling", + "language": "Python", "votes": 1056, "comments": 91, "medal": "silver", + "best_score": None, "runtime": "38.5s", "last_run": "2026-05-29", + "linked_competition": None, "linked_dataset": "global-co2-emissions-1960-2025", + "thumbnail": "co2-emissions-trends-eda", "tags": ["eda", "climate", "visualization", "storytelling"], + "description": "A narrative EDA of 65 years of global CO₂ emissions: who emits the most per capita, how emissions track GDP, and which countries have decoupled growth from carbon.", + }, + { + "slug": "wheat-yield-lgbm-geospatial", "author": "sara_timeseries", + "title": "Wheat Yield — Geospatial Features + LightGBM", + "language": "Python", "votes": 388, "comments": 44, "medal": "bronze", + "best_score": "1.0423", "runtime": "266.0s", "last_run": "2026-05-12", + "linked_competition": "global-wheat-yield-forecast", "linked_dataset": "global-wheat-satellite-imagery", + "thumbnail": "wheat-yield-lgbm-geospatial", "tags": ["geospatial", "lightgbm", "feature engineering", "agriculture"], + "description": "Extracts NDVI time-series and weather aggregates from the satellite patches, then trains a LightGBM regressor to forecast end-of-season wheat yield. Strong public-LB baseline.", + }, + { + "slug": "happiness-report-regression", "author": "bobsmith_ml", + "title": "What Makes Countries Happy? A Regression Study", + "language": "Python", "votes": 503, "comments": 47, "medal": "bronze", + "best_score": None, "runtime": "21.7s", "last_run": "2026-03-22", + "linked_competition": None, "linked_dataset": "world-happiness-report-2026", + "thumbnail": "happiness-report-regression", "tags": ["eda", "regression", "social science", "beginner"], + "description": "Explores which factors best predict national happiness using linear and tree models, with partial-dependence plots that quantify the contribution of GDP, social support, and freedom.", + }, +] + +# ---------------------------------------------------------------------------- +# Models +# slug, title, owner_username, framework, variations, downloads, upvotes, +# license, tags, last_updated, thumbnail, description +# ---------------------------------------------------------------------------- +MODELS = [ + { + "slug": "resnet50-chestxray", "owner": "kenji_cv", + "title": "ResNet-50 Chest X-Ray Classifier", + "framework": "PyTorch", "variations": 3, "downloads": 41200, "upvotes": 612, + "license": "Apache 2.0", "tags": ["computer vision", "medical imaging", "classification"], + "last_updated": "2026-04-18", "thumbnail": "resnet50-chestxray", + "description": "A ResNet-50 fine-tuned on the Chest X-Ray Pneumonia dataset, with fp16 and int8 variations for edge deployment. Reaches 0.97 ROC AUC on the held-out test split.", + }, + { + "slug": "distilbert-imdb-sentiment", "owner": "marco_nlp", + "title": "DistilBERT IMDB Sentiment", + "framework": "Transformers", "variations": 2, "downloads": 88700, "upvotes": 901, + "license": "MIT", "tags": ["nlp", "sentiment", "text classification"], + "last_updated": "2025-11-16", "thumbnail": "distilbert-imdb-sentiment", + "description": "DistilBERT fine-tuned on IMDB 50K reviews for binary sentiment, 94% accuracy. Includes base and quantized ONNX variations.", + }, + { + "slug": "lightgbm-fraud-detector", "owner": "raul_gbm", + "title": "LightGBM Fraud Detector", + "framework": "scikit-learn", "variations": 1, "downloads": 22300, "upvotes": 388, + "license": "Apache 2.0", "tags": ["tabular", "finance", "classification", "imbalanced"], + "last_updated": "2026-06-21", "thumbnail": "lightgbm-fraud-detector", + "description": "A serialized LightGBM model + preprocessing pipeline for card-fraud scoring, trained on the Credit Card Fraud Transactions dataset. PR-AUC 0.91.", + }, + { + "slug": "unet-leaf-segmentation", "owner": "kenji_cv", + "title": "U-Net Leaf Disease Segmenter", + "framework": "PyTorch", "variations": 2, "downloads": 9800, "upvotes": 201, + "license": "CC BY 4.0", "tags": ["computer vision", "segmentation", "agriculture"], + "last_updated": "2026-05-06", "thumbnail": "unet-leaf-segmentation", + "description": "U-Net with a ResNet-34 encoder trained on the Plant Leaf Disease Images dataset for pixel-level lesion segmentation. Dice 0.88 on validation.", + }, + { + "slug": "tabnet-credit-risk", "owner": "psi_grandmaster", + "title": "TabNet Credit Risk Model", + "framework": "PyTorch", "variations": 1, "downloads": 15600, "upvotes": 277, + "license": "Apache 2.0", "tags": ["tabular", "finance", "classification", "deep learning"], + "last_updated": "2026-06-16", "thumbnail": "tabnet-credit-risk", + "description": "An attention-based TabNet model for the Home Credit Default Risk problem, with feature-importance masks for interpretability. ROC AUC 0.80.", + }, + { + "slug": "prophet-energy-forecaster", "owner": "sara_timeseries", + "title": "Prophet City Energy Forecaster", + "framework": "scikit-learn", "variations": 1, "downloads": 7400, "upvotes": 142, + "license": "MIT", "tags": ["time series", "energy", "forecasting"], + "last_updated": "2026-05-20", "thumbnail": "prophet-energy-forecaster", + "description": "A tuned Prophet model with custom holiday and weather regressors for hourly metropolitan electricity-load forecasting. MAPE 3.1% on the validation horizon.", + }, + { + "slug": "efficientnet-leaf-classifier", "owner": "datasmith_io", + "title": "EfficientNet-B3 Plant Leaf Classifier", + "framework": "TensorFlow", "variations": 2, "downloads": 12800, "upvotes": 188, + "license": "Apache 2.0", "tags": ["computer vision", "agriculture", "image classification"], + "last_updated": "2026-03-28", "thumbnail": "efficientnet-leaf-classifier", + "description": "An EfficientNet-B3 image classifier trained on the Plant Leaf Disease Images dataset to recognise 26 disease classes across 12 crops. TensorFlow SavedModel and TFLite variations included.", + }, + { + "slug": "yolov8-traffic-detector", "owner": "carolwong", + "title": "YOLOv8 Urban Traffic Object Detector", + "framework": "PyTorch", "variations": 3, "downloads": 33100, "upvotes": 421, + "license": "GPL 2", "tags": ["computer vision", "object detection", "geospatial"], + "last_updated": "2026-06-02", "thumbnail": "yolov8-traffic-detector", + "description": "A YOLOv8 detector fine-tuned to spot cars, buses, cyclists, and pedestrians in dashcam and CCTV frames. Nano, small, and medium variations for different latency budgets.", + }, + { + "slug": "bert-ner-finance", "owner": "marco_nlp", + "title": "BERT Financial NER", + "framework": "Transformers", "variations": 1, "downloads": 19400, "upvotes": 256, + "license": "MIT", "tags": ["nlp", "named entity recognition", "finance"], + "last_updated": "2026-04-09", "thumbnail": "bert-ner-finance", + "description": "A BERT-base token-classification model that tags companies, tickers, monetary amounts, and dates in financial news and filings.", + }, + { + "slug": "xgboost-churn-predictor", "owner": "psi_grandmaster", + "title": "XGBoost Customer Churn Predictor", + "framework": "scikit-learn", "variations": 1, "downloads": 10250, "upvotes": 174, + "license": "Apache 2.0", "tags": ["tabular", "business", "classification"], + "last_updated": "2026-05-31", "thumbnail": "xgboost-churn-predictor", + "description": "A gradient-boosted churn classifier with SHAP explanations, trained on telco-style subscriber data. Ships with a calibrated probability head.", + }, +] + +# ---------------------------------------------------------------------------- +# Kaggle Learn courses +# slug, title, lessons, hours, level, icon, tags, description, lesson_titles +# ---------------------------------------------------------------------------- +COURSES = [ + { + "slug": "intro-to-machine-learning", "title": "Intro to Machine Learning", + "lessons": 7, "hours": 3, "level": "Beginner", "icon": "🤖", + "tags": ["machine learning", "beginner", "scikit-learn"], + "description": "Learn the core ideas in machine learning and build your first models. Decision trees, model validation, underfitting vs overfitting, and random forests.", + "lesson_titles": ["How Models Work", "Basic Data Exploration", "Your First Machine Learning Model", + "Model Validation", "Underfitting and Overfitting", "Random Forests", "Machine Learning Competitions"], + }, + { + "slug": "pandas", "title": "Pandas", + "lessons": 6, "hours": 4, "level": "Beginner", "icon": "🐼", + "tags": ["pandas", "data manipulation", "python"], + "description": "Solve short hands-on challenges to perfect your data-manipulation skills with pandas — the most important Python library for working with tabular data.", + "lesson_titles": ["Creating, Reading and Writing", "Indexing, Selecting & Assigning", "Summary Functions and Maps", + "Grouping and Sorting", "Data Types and Missing Values", "Renaming and Combining"], + }, + { + "slug": "intro-to-deep-learning", "title": "Intro to Deep Learning", + "lessons": 6, "hours": 4, "level": "Intermediate", "icon": "🧠", + "tags": ["deep learning", "keras", "neural networks"], + "description": "Use TensorFlow and Keras to build and train neural networks for structured data. Stochastic gradient descent, overfitting, dropout, and batch normalization.", + "lesson_titles": ["A Single Neuron", "Deep Neural Networks", "Stochastic Gradient Descent", + "Overfitting and Underfitting", "Dropout and Batch Normalization", "Binary Classification"], + }, + { + "slug": "computer-vision", "title": "Computer Vision", + "lessons": 6, "hours": 4, "level": "Intermediate", "icon": "👁️", + "tags": ["computer vision", "cnn", "keras", "deep learning"], + "description": "Build convolutional neural networks with TensorFlow and Keras. Learn about convolution, pooling, data augmentation, and transfer learning for image classification.", + "lesson_titles": ["The Convolutional Classifier", "Convolution and ReLU", "Maximum Pooling", + "The Sliding Window", "Custom Convnets", "Data Augmentation"], + }, + { + "slug": "feature-engineering", "title": "Feature Engineering", + "lessons": 6, "hours": 5, "level": "Intermediate", "icon": "🛠️", + "tags": ["feature engineering", "machine learning", "tabular"], + "description": "Better features make better models. Discover how to engineer features, measure mutual information, create features, and use target encoding and clustering.", + "lesson_titles": ["What Is Feature Engineering", "Mutual Information", "Creating Features", + "Clustering With K-Means", "Principal Component Analysis", "Target Encoding"], + }, + { + "slug": "intro-to-sql", "title": "Intro to SQL", + "lessons": 6, "hours": 3, "level": "Beginner", "icon": "🗄️", + "tags": ["sql", "bigquery", "data"], + "description": "Learn SQL for working with databases using Google BigQuery. SELECT, GROUP BY, ORDER BY, joins, and how to keep your queries under control.", + "lesson_titles": ["Getting Started With SQL and BigQuery", "Select, From & Where", "Group By, Having & Count", + "Order By", "As & With", "Joining Data"], + }, + { + "slug": "data-visualization", "title": "Data Visualization", + "lessons": 7, "hours": 4, "level": "Beginner", "icon": "📊", + "tags": ["visualization", "seaborn", "eda"], + "description": "Make great data visualizations with seaborn. Line, bar, and scatter charts, distributions, and choosing the right plot for your data and story.", + "lesson_titles": ["Hello, Seaborn", "Line Charts", "Bar Charts and Heatmaps", "Scatter Plots", + "Distributions", "Choosing Plot Types and Custom Styles", "Final Project"], + }, + { + "slug": "intro-to-deep-learning-nlp", "title": "Natural Language Processing", + "lessons": 4, "hours": 3, "level": "Advanced", "icon": "💬", + "tags": ["nlp", "transformers", "text", "deep learning"], + "description": "Distinguish yourself by learning to work with text data. Text classification, word vectors, and building NLP pipelines with spaCy and transformers.", + "lesson_titles": ["Intro to NLP", "Text Classification", "Word Vectors", "Transformers and Beyond"], + }, + { + "slug": "time-series", "title": "Time Series", + "lessons": 6, "hours": 4, "level": "Intermediate", "icon": "📈", + "tags": ["time series", "forecasting", "regression"], + "description": "Apply machine learning to real-world forecasting tasks. Trend and seasonality, time-series features, hybrid models, and forecasting with ML.", + "lesson_titles": ["Linear Regression With Time Series", "Trend", "Seasonality", + "Time Series as Features", "Hybrid Models", "Forecasting With Machine Learning"], + }, + { + "slug": "intro-to-ai-ethics", "title": "Intro to AI Ethics", + "lessons": 5, "hours": 4, "level": "Beginner", "icon": "⚖️", + "tags": ["ai ethics", "fairness", "responsible ai"], + "description": "Explore practical tools to guide the moral design of AI systems. Human-centered design, bias, fairness, model cards, and AI fairness metrics.", + "lesson_titles": ["Introduction", "Human-Centered Design for AI", "Identifying Bias in AI", + "AI Fairness", "Model Cards"], + }, +] + +# ---------------------------------------------------------------------------- +# Discussions (forum threads) +# slug, title, author_username, forum, votes, comments, pinned, created_at, body +# ---------------------------------------------------------------------------- +DISCUSSIONS = [ + { + "slug": "welcome-to-kaggle-getting-started", "author": "Kaggle", + "title": "Welcome to Kaggle! Start here", "forum": "Getting Started", + "votes": 5210, "comments": 642, "pinned": True, "created_at": "2025-01-05", + "body": "New to Kaggle? This thread links the essentials: pick a Getting Started competition like Titanic, take the Intro to Machine Learning course on Kaggle Learn, and don't be afraid to fork a public notebook. Welcome aboard!", + }, + { + "slug": "fraud-detection-pr-auc-vs-roc-auc", "author": "raul_gbm", + "title": "Why PR-AUC, not ROC-AUC, for the fraud comp?", "forum": "Questions & Answers", + "votes": 412, "comments": 58, "pinned": False, "created_at": "2026-06-08", + "body": "With fraud at 0.17% prevalence, ROC-AUC looks great even for weak models because true negatives dominate. PR-AUC focuses on the positive (fraud) class, so it's the honest metric here. Curious how others are calibrating their thresholds.", + }, + { + "slug": "credit-default-leak-warning", "author": "psi_grandmaster", + "title": "Heads up: potential target leak via SK_ID ordering", "forum": "Competition Hosting", + "votes": 388, "comments": 71, "pinned": True, "created_at": "2026-06-11", + "body": "If you sort by SK_ID_CURR you'll notice the default rate drifts. That's an artifact of how the data was exported, not signal. Using row order as a feature will overfit the public LB and collapse on private. Don't do it.", + }, + { + "slug": "best-gpu-for-kaggle-2026", "author": "bobsmith_ml", + "title": "Best free GPU setup for training in 2026?", "forum": "General", + "votes": 244, "comments": 89, "pinned": False, "created_at": "2026-05-19", + "body": "Kaggle gives 30 GPU hours/week on T4s and P100s. For the chest X-ray comp that's tight. Anyone splitting training across sessions or using gradient checkpointing to fit U-Net at higher resolution?", + }, + { + "slug": "feature-request-dark-mode", "author": "tomeka_viz", + "title": "Feature request: dark mode for notebooks", "forum": "Product Feedback", + "votes": 1820, "comments": 203, "pinned": False, "created_at": "2026-04-12", + "body": "Please ship a real dark mode for the notebook editor. My eyes during a 2am competition crunch would appreciate it. Upvote if you agree!", + }, + { + "slug": "sharing-co2-dataset-v3", "author": "datasmith_io", + "title": "[Dataset] Global CO₂ Emissions updated through 2025", "forum": "Datasets", + "votes": 302, "comments": 41, "pinned": False, "created_at": "2026-05-28", + "body": "Just pushed v3 of the Global CO₂ Emissions dataset with 2025 figures and a cleaned per-capita column. Usability is back to 10.0. Let me know if you spot any country-code mismatches.", + }, + { + "slug": "wheat-yield-ndvi-features", "author": "sara_timeseries", + "title": "What NDVI aggregation works best for wheat yield?", "forum": "Questions & Answers", + "votes": 156, "comments": 34, "pinned": False, "created_at": "2026-05-13", + "body": "I'm getting the best CV from max-NDVI in the heading stage plus the integral of NDVI over the season. Mean NDVI alone underperforms. What growth-stage windows are working for you?", + }, + { + "slug": "titanic-feature-ideas", "author": "carolwong", + "title": "Underrated Titanic features that actually help", "forum": "Getting Started", + "votes": 921, "comments": 167, "pinned": False, "created_at": "2026-05-22", + "body": "Beyond the obvious: extracting the title from Name (Mr/Mrs/Master/Rare), family size = SibSp+Parch+1, and a 'deck' letter from the cabin all move the needle. Ticket-group survival is the sneaky strong one.", + }, +] + +# Comments attached to discussions. slug -> list of (author_username, body, votes, created_at) +DISCUSSION_COMMENTS = { + "fraud-detection-pr-auc-vs-roc-auc": [ + ("psi_grandmaster", "Exactly. I tune the threshold to maximize F1 on out-of-fold predictions, then sanity-check the PR curve.", 41, "2026-06-08"), + ("alicejdata", "Class weights in LightGBM plus PR-AUC early stopping got me to 0.88. Thanks for the writeup!", 22, "2026-06-09"), + ], + "titanic-feature-ideas": [ + ("bobsmith_ml", "The title extraction alone bumped me from 0.76 to 0.79. Wild how much a regex helps.", 58, "2026-05-23"), + ("davidtran", "Total beginner here — thank you, the family-size feature finally got me off 0.62.", 12, "2026-05-24"), + ], +} diff --git a/sites/kaggle/static/css/.gitkeep b/sites/kaggle/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/static/css/main.css b/sites/kaggle/static/css/main.css new file mode 100644 index 00000000..802f83cf --- /dev/null +++ b/sites/kaggle/static/css/main.css @@ -0,0 +1,208 @@ +/* Kaggle mirror — styling approximating kaggle.com's Material-ish look. */ +:root { + --kaggle-cyan: #20beff; + --kaggle-cyan-dark: #1ba7e0; + --ink: #202124; + --ink-soft: #5f6368; + --line: #e0e0e0; + --bg: #ffffff; + --bg-soft: #f5f5f5; + --bg-rail: #fafafa; + --gold: #dca917; + --silver: #b5b5b5; + --bronze: #c08851; + --radius: 12px; + --shadow: 0 1px 2px rgba(60,64,67,.1), 0 1px 3px rgba(60,64,67,.08); + --shadow-hover: 0 2px 6px rgba(60,64,67,.18), 0 1px 4px rgba(60,64,67,.12); + --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--font); + color: var(--ink); + background: var(--bg); + font-size: 14px; + line-height: 1.5; +} +a { color: var(--kaggle-cyan-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +img { max-width: 100%; } +h1, h2, h3, h4 { color: var(--ink); margin: 0 0 .5em; font-weight: 600; } + +.container { max-width: 1080px; margin: 0 auto; padding: 0 24px; } +.container-wide { max-width: 1280px; margin: 0 auto; padding: 0 24px; } + +/* ---------- Top nav ---------- */ +.gn { position: sticky; top: 0; z-index: 50; background: #fff; border-bottom: 1px solid var(--line); height: 56px; } +.gn-inner { max-width: 1280px; margin: 0 auto; padding: 0 20px; height: 56px; display: flex; align-items: center; gap: 16px; } +.gn-logo { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 18px; color: var(--ink); } +.gn-logo:hover { text-decoration: none; } +.gn-logo svg { height: 26px; width: auto; display: block; } +.gn-search { flex: 1; max-width: 520px; } +.gn-search input { + width: 100%; height: 38px; border: 1px solid var(--line); border-radius: 999px; + padding: 0 16px; font-size: 14px; background: var(--bg-soft); outline: none; +} +.gn-search input:focus { background: #fff; border-color: var(--kaggle-cyan); } +.gn-nav { display: flex; gap: 4px; align-items: center; } +.gn-nav a { color: var(--ink-soft); padding: 8px 10px; border-radius: 6px; font-weight: 500; } +.gn-nav a:hover { background: var(--bg-soft); color: var(--ink); text-decoration: none; } +.gn-auth { display: flex; gap: 8px; align-items: center; } +.gn-avatar img { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1px solid var(--line); display: block; } + +/* ---------- Buttons ---------- */ +.btn { display: inline-flex; align-items: center; gap: 6px; border: 1px solid transparent; border-radius: 999px; + padding: 8px 18px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; } +.btn:hover { text-decoration: none; } +.btn-sm { padding: 6px 14px; font-size: 13px; } +.btn-primary { background: var(--kaggle-cyan); color: #fff; } +.btn-primary:hover { background: var(--kaggle-cyan-dark); color: #fff; } +.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line); } +.btn-ghost:hover { background: var(--bg-soft); } +.btn-outline { background: #fff; color: var(--kaggle-cyan-dark); border-color: var(--kaggle-cyan); } +.btn-outline:hover { background: #eafaff; } +.btn-block { width: 100%; justify-content: center; } + +/* ---------- Flash ---------- */ +.flash-wrap { max-width: 1080px; margin: 12px auto 0; padding: 0 24px; } +.flash { padding: 10px 16px; border-radius: 8px; margin-bottom: 8px; font-weight: 500; } +.flash-success { background: #e6f8ee; color: #137333; border: 1px solid #b6e6c9; } +.flash-error { background: #fce8e6; color: #c5221f; border: 1px solid #f5c2bd; } +.flash-info { background: #e8f0fe; color: #1967d2; border: 1px solid #c2d7fb; } + +/* ---------- Hero ---------- */ +.hero { background: linear-gradient(120deg, #0b2c3d 0%, #114a63 50%, #1ba7e0 100%); color: #fff; padding: 56px 0; } +.hero h1 { color: #fff; font-size: 38px; margin-bottom: 12px; } +.hero p { font-size: 17px; color: #d6eefb; max-width: 640px; margin: 0 0 24px; } +.hero .btn-primary { background: #fff; color: #114a63; } +.hero .btn-primary:hover { background: #eafaff; } + +/* ---------- Sections ---------- */ +.section { padding: 36px 0; } +.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 18px; } +.section-head h2 { font-size: 22px; margin: 0; } +.section-head a { font-size: 14px; font-weight: 600; } + +/* ---------- Cards / grids ---------- */ +.grid { display: grid; gap: 16px; } +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } +@media (max-width: 980px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 620px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } } + +.card { background: #fff; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); transition: box-shadow .15s, transform .15s; } +.card:hover { box-shadow: var(--shadow-hover); } +.card a { color: inherit; } +.card-thumb { width: 100%; height: 130px; object-fit: cover; background: var(--bg-soft); display: block; } +.card-thumb-tall { height: 160px; } +.card-body { padding: 14px 16px; } +.card-title { font-size: 15px; font-weight: 600; margin: 0 0 4px; line-height: 1.35; } +.card-sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 10px; } +.card-meta { display: flex; flex-wrap: wrap; gap: 12px; color: var(--ink-soft); font-size: 12.5px; } +.card-meta b { color: var(--ink); font-weight: 600; } + +/* gradient placeholder shown if a real thumbnail is missing */ +.thumb-fallback { display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; + font-size: 22px; background: linear-gradient(135deg, #20beff, #114a63); } + +/* ---------- Tags / chips ---------- */ +.chips { display: flex; flex-wrap: wrap; gap: 6px; } +.chip { display: inline-block; background: var(--bg-soft); color: var(--ink-soft); border-radius: 999px; + padding: 3px 10px; font-size: 12px; font-weight: 500; } +.chip-link:hover { background: #e3f4fc; color: var(--kaggle-cyan-dark); text-decoration: none; } +.chip-active { background: var(--kaggle-cyan); color: #fff; } + +/* ---------- Tier badge ---------- */ +.tier { display: inline-flex; align-items: center; gap: 5px; font-weight: 700; font-size: 12.5px; } +.tier-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; } +.medal { font-size: 13px; } +.medal-counts { display: inline-flex; gap: 10px; align-items: center; } + +/* ---------- List rows ---------- */ +.row-list { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: #fff; } +.row { display: flex; gap: 14px; padding: 14px 18px; border-bottom: 1px solid var(--line); align-items: center; } +.row:last-child { border-bottom: none; } +.row:hover { background: var(--bg-rail); } +.row-thumb { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; flex-shrink: 0; background: var(--bg-soft); } +.row-main { flex: 1; min-width: 0; } +.row-title { font-size: 15px; font-weight: 600; margin: 0 0 2px; } +.row-sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 6px; } +.row-right { text-align: right; color: var(--ink-soft); font-size: 13px; white-space: nowrap; } + +/* ---------- Filter bar ---------- */ +.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 18px; } +.toolbar form { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } +.toolbar select, .toolbar input[type=text] { + height: 36px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; font-size: 13px; background: #fff; font-family: inherit; } +.page-title { font-size: 26px; margin: 0 0 4px; } +.page-sub { color: var(--ink-soft); margin: 0 0 18px; } +.count-pill { color: var(--ink-soft); font-size: 13px; font-weight: 500; } + +/* ---------- Detail layout ---------- */ +.detail-head { display: flex; gap: 20px; align-items: flex-start; padding: 28px 0 20px; border-bottom: 1px solid var(--line); } +.detail-thumb { width: 110px; height: 110px; border-radius: 14px; object-fit: cover; flex-shrink: 0; background: var(--bg-soft); } +.detail-title { font-size: 28px; margin: 0 0 6px; } +.detail-sub { color: var(--ink-soft); font-size: 16px; margin: 0 0 12px; } +.detail-cols { display: grid; grid-template-columns: 1fr 300px; gap: 28px; padding: 24px 0; } +@media (max-width: 900px) { .detail-cols { grid-template-columns: 1fr; } } +.side-box { border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; margin-bottom: 16px; background: #fff; } +.side-box h4 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); margin: 0 0 10px; } +.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; } +.stat { } +.stat-num { font-size: 20px; font-weight: 700; } +.stat-label { color: var(--ink-soft); font-size: 12px; } +.prose { font-size: 15px; line-height: 1.7; color: #3c4043; } +.prose p { margin: 0 0 14px; } + +.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 8px 0 20px; } +.tabs a { padding: 10px 16px; color: var(--ink-soft); font-weight: 600; border-bottom: 2px solid transparent; } +.tabs a:hover { text-decoration: none; color: var(--ink); } +.tabs a.active { color: var(--kaggle-cyan-dark); border-bottom-color: var(--kaggle-cyan); } + +/* ---------- Tables ---------- */ +table.lb { width: 100%; border-collapse: collapse; font-size: 14px; } +table.lb th, table.lb td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); } +table.lb th { color: var(--ink-soft); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; } +table.lb tr:hover td { background: var(--bg-rail); } +.rank-medal { font-weight: 700; } + +/* ---------- Forms ---------- */ +.form-card { max-width: 420px; margin: 48px auto; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); padding: 32px; box-shadow: var(--shadow); } +.form-card.wide { max-width: 620px; } +.form-card h1 { font-size: 24px; text-align: center; margin-bottom: 8px; } +.form-card .sub { text-align: center; color: var(--ink-soft); margin-bottom: 24px; } +.field { margin-bottom: 16px; } +.field label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 6px; } +.field input, .field textarea, .field select { + width: 100%; border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font-size: 14px; font-family: inherit; } +.field input:focus, .field textarea:focus { outline: none; border-color: var(--kaggle-cyan); } +.field .err { color: #c5221f; font-size: 12px; margin-top: 4px; } +.muted { color: var(--ink-soft); } +.center { text-align: center; } + +/* ---------- Profile ---------- */ +.profile-head { display: flex; gap: 22px; align-items: center; padding: 28px 0; } +.profile-avatar { width: 110px; height: 110px; border-radius: 50%; object-fit: cover; border: 3px solid #fff; box-shadow: var(--shadow); } +.profile-name { font-size: 26px; margin: 0; } +.profile-handle { color: var(--ink-soft); font-size: 15px; } + +/* ---------- Vote button ---------- */ +.vote-btn { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); background: #fff; + border-radius: 999px; padding: 6px 14px; font-weight: 600; font-size: 13px; cursor: pointer; color: var(--ink); font-family: inherit; } +.vote-btn:hover { border-color: var(--kaggle-cyan); color: var(--kaggle-cyan-dark); } +.vote-btn.voted { background: #eafaff; border-color: var(--kaggle-cyan); color: var(--kaggle-cyan-dark); } + +/* ---------- Footer ---------- */ +.footer { border-top: 1px solid var(--line); margin-top: 48px; padding: 36px 0 28px; background: var(--bg-rail); } +.footer-cols { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 24px; } +@media (max-width: 760px) { .footer-cols { grid-template-columns: 1fr 1fr; } } +.footer h4 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); } +.footer a { display: block; color: var(--ink-soft); padding: 3px 0; font-size: 13px; } +.footer a:hover { color: var(--kaggle-cyan-dark); } +.footer-brand-logo { font-weight: 700; font-size: 18px; display: flex; align-items: center; gap: 6px; } +.footer-bottom { display: flex; justify-content: space-between; color: var(--ink-soft); font-size: 12.5px; margin-top: 24px; border-top: 1px solid var(--line); padding-top: 16px; } + +.empty { text-align: center; color: var(--ink-soft); padding: 48px 0; } diff --git a/sites/kaggle/static/icons/.gitkeep b/sites/kaggle/static/icons/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/static/js/.gitkeep b/sites/kaggle/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/static/js/main.js b/sites/kaggle/static/js/main.js new file mode 100644 index 00000000..84524193 --- /dev/null +++ b/sites/kaggle/static/js/main.js @@ -0,0 +1,60 @@ +// Kaggle mirror — lightweight interactions (vote / bookmark / follow via fetch). +(function () { + const csrf = document.querySelector('meta[name="csrf-token"]'); + const token = csrf ? csrf.getAttribute('content') : ''; + + function post(url, data) { + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRFToken': token, + }, + body: JSON.stringify(data), + }).then(r => r.json()); + } + + document.addEventListener('click', function (e) { + const vb = e.target.closest('[data-vote]'); + if (vb) { + e.preventDefault(); + post('/api/vote', { entity_type: vb.dataset.type, entity_id: vb.dataset.id }) + .then(res => { + if (res.error) { window.location = '/login'; return; } + vb.classList.toggle('voted', res.voted); + const cnt = vb.querySelector('[data-count]'); + if (cnt && res.count != null) cnt.textContent = res.count.toLocaleString(); + const lbl = vb.querySelector('[data-label]'); + if (lbl) lbl.textContent = res.voted ? 'Voted' : 'Upvote'; + }) + .catch(() => { window.location = '/login'; }); + return; + } + const bb = e.target.closest('[data-bookmark]'); + if (bb) { + e.preventDefault(); + post('/api/bookmark', { entity_type: bb.dataset.type, entity_id: bb.dataset.id }) + .then(res => { + if (res.error) { window.location = '/login'; return; } + bb.classList.toggle('voted', res.bookmarked); + const lbl = bb.querySelector('[data-label]'); + if (lbl) lbl.textContent = res.bookmarked ? 'Saved' : 'Save'; + }) + .catch(() => { window.location = '/login'; }); + return; + } + const fb = e.target.closest('[data-follow]'); + if (fb) { + e.preventDefault(); + post('/api/follow', { username: fb.dataset.username }) + .then(res => { + if (res.error) { window.location = '/login'; return; } + fb.classList.toggle('voted', res.following); + fb.textContent = res.following ? 'Following' : 'Follow'; + }) + .catch(() => { window.location = '/login'; }); + return; + } + }); +})(); diff --git a/sites/kaggle/tasks.jsonl b/sites/kaggle/tasks.jsonl new file mode 100644 index 00000000..ced8475c --- /dev/null +++ b/sites/kaggle/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name": "Kaggle", "id": "Kaggle--0", "ques": "Find the Getting Started competition about predicting passenger survival on the Titanic and open its page. What is the evaluation metric used?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--1", "ques": "Open the 'Home Credit Default Risk 2026' competition and report which evaluation metric is used to score submissions.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--2", "ques": "Among the datasets tagged 'climate', which one has the most upvotes? Give its title.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--3", "ques": "Find the 'Credit Card Fraud Transactions' dataset and download it.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--4", "ques": "On the 'Home Credit Default Risk 2026' competition leaderboard, which team is ranked #1 and what is their score?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--5", "ques": "Find a PyTorch model for chest X-ray classification and report the license it is released under.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--6", "ques": "Sign in with email alice.j@test.com and password TestPass123!, then join the 'LLM Prompt Recovery' competition with the team name 'Data Wizards'.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--7", "ques": "Sign in with email alice.j@test.com and password TestPass123!, then upvote the dataset 'World Happiness Report 2026'.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--8", "ques": "Sign in with email bob.c@test.com and password TestPass123!, then save (bookmark) the notebook 'Titanic — Top 3% Solution Walkthrough' to your account.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--9", "ques": "Sign in with email bob.c@test.com and password TestPass123!, then follow the user 'psi_grandmaster'.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--10", "ques": "Sign in with email alice.j@test.com and password TestPass123!, then start a new discussion in the 'Questions & Answers' forum titled 'How do you handle class imbalance?' with a short body.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--11", "ques": "Sign in with email david.k@test.com and password TestPass123!, open the discussion 'Underrated Titanic features that actually help', and post a comment thanking the author.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--12", "ques": "Sign in with email david.k@test.com and password TestPass123!, then update your profile location to 'Boston, United States'.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--13", "ques": "Open the Notebooks rankings page. Who is the top-ranked user (rank #1)?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--14", "ques": "On Kaggle Learn, how many lessons does the 'Intro to Machine Learning' course contain?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--15", "ques": "Open the 'Credit Card Fraud Transactions' dataset, find a public notebook that uses it, open that notebook, and report its best leaderboard score.", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--16", "ques": "Among the Featured competitions that offer a cash prize, which one has the largest reward, and how much is it?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--17", "ques": "Look at the competitions hosted by the user 'sara_timeseries'. Which of her hosted competitions has the earliest deadline?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--18", "ques": "Open the 'MNIST Handwritten Digits' dataset. Under what license is it released?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} +{"web_name": "Kaggle", "id": "Kaggle--19", "ques": "Among Python notebooks that earned a gold medal, which one has the most votes, and who is its author?", "web": "http://localhost:40015/", "upstream_url": "https://www.kaggle.com/"} diff --git a/sites/kaggle/templates/.gitkeep b/sites/kaggle/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/templates/404.html b/sites/kaggle/templates/404.html new file mode 100644 index 00000000..70f7cd2e --- /dev/null +++ b/sites/kaggle/templates/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Not Found | Kaggle{% endblock %} +{% block content %} +
+

404

+

We couldn't find that page.

+ Back to Home +
+{% endblock %} diff --git a/sites/kaggle/templates/500.html b/sites/kaggle/templates/500.html new file mode 100644 index 00000000..9c84719a --- /dev/null +++ b/sites/kaggle/templates/500.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Error | Kaggle{% endblock %} +{% block content %} +
+

500

+

Something went wrong on our end.

+ Back to Home +
+{% endblock %} diff --git a/sites/kaggle/templates/_macros.html b/sites/kaggle/templates/_macros.html new file mode 100644 index 00000000..0bcfebf8 --- /dev/null +++ b/sites/kaggle/templates/_macros.html @@ -0,0 +1,89 @@ +{# Reusable card / badge macros #} +{% set TIER_COLORS = {"Novice": "#5ac995", "Contributor": "#00aaff", "Expert": "#95319b", "Master": "#f96517", "Grandmaster": "#dca917"} %} + +{% macro thumb(kind, key, title, cls='card-thumb') %} + {{ title }} +{% endmacro %} + +{% macro tier_badge(user) %} + {% if user %} + + {{ user.tier }} + + {% endif %} +{% endmacro %} + +{% macro tier_name(name) %} + + {{ name }} + +{% endmacro %} + +{% macro medals(g, s, b) %} + + 🥇 {{ g }} + 🥈 {{ s }} + 🥉 {{ b }} + +{% endmacro %} + +{% macro competition_card(c) %} + +{% endmacro %} + +{% macro dataset_card(d) %} + +{% endmacro %} + +{% macro notebook_card(n) %} + +{% endmacro %} + +{% macro model_card(m) %} + +{% endmacro %} diff --git a/sites/kaggle/templates/account.html b/sites/kaggle/templates/account.html new file mode 100644 index 00000000..14a374e6 --- /dev/null +++ b/sites/kaggle/templates/account.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_badge, medals %} +{% block title %}Your Account | Kaggle{% endblock %} +{% block content %} +
+
+ +
+

{{ current_user.display_name or current_user.username }} {{ tier_badge(current_user) }}

+
@{{ current_user.username }} · {{ current_user.email }}
+
{{ medals(current_user.gold, current_user.silver, current_user.bronze) }} · {{ current_user.points|commafy }} points
+
+ +
+ +
+

My Competitions ({{ entries|length }})

+ {% if entries %} +
{% for e in entries %} +
+ +
Team: {{ e.team_name }} · joined {{ e.joined_at|reltime }}
+
{{ e.competition.reward }}
+ {% endfor %}
+ {% else %}
You haven't joined any competitions yet. Browse competitions
{% endif %} +
+ +
+

Saved ({{ bookmarks|length }})

+ {% if bookmarks %} +
{% for ent, b in bookmarks %} +
+
{{ ent.title }}
+
{{ b.entity_type }}
+
+ {% endfor %}
+ {% else %}
No saved items yet.
{% endif %} +
+ +
+

Following ({{ follows|length }})

+ {% if follows %} +
{% for f in follows %}@{{ f.target_username }}{% endfor %}
+ {% else %}
You're not following anyone yet.
{% endif %} +
+ +
+
+ + +
+
+
+{% endblock %} diff --git a/sites/kaggle/templates/account_edit.html b/sites/kaggle/templates/account_edit.html new file mode 100644 index 00000000..1b6a3a61 --- /dev/null +++ b/sites/kaggle/templates/account_edit.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Edit Profile | Kaggle{% endblock %} +{% block content %} +
+

Edit Profile

+
+ {{ form.csrf_token }} +
{{ form.display_name() }}
+
{{ form.bio(rows=4) }}
+
{{ form.location() }}
+
{{ form.occupation() }}
+
{{ form.organization() }}
+
{{ form.website() }}
+ +
+

← Back to account

+
+{% endblock %} diff --git a/sites/kaggle/templates/base.html b/sites/kaggle/templates/base.html new file mode 100644 index 00000000..b3cbb160 --- /dev/null +++ b/sites/kaggle/templates/base.html @@ -0,0 +1,99 @@ + + + + + + + {% block title %}Kaggle: Your Machine Learning and Data Science Community{% endblock %} + + + + +{% macro kaggle_logo() %} + + + + + kaggle + +{% endmacro %} + + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} +{% endwith %} + +{% block content %}{% endblock %} + + + + +{% block scripts %}{% endblock %} + + diff --git a/sites/kaggle/templates/change_password.html b/sites/kaggle/templates/change_password.html new file mode 100644 index 00000000..ffdb1dfa --- /dev/null +++ b/sites/kaggle/templates/change_password.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Change Password | Kaggle{% endblock %} +{% block content %} +
+

Change Password

+
+ {{ form.csrf_token }} +
{{ form.current() }} + {% for e in form.current.errors %}
{{ e }}
{% endfor %}
+
{{ form.new() }} + {% for e in form.new.errors %}
{{ e }}
{% endfor %}
+
{{ form.confirm() }} + {% for e in form.confirm.errors %}
{{ e }}
{% endfor %}
+ +
+

← Back to account

+
+{% endblock %} diff --git a/sites/kaggle/templates/competition_detail.html b/sites/kaggle/templates/competition_detail.html new file mode 100644 index 00000000..fd03048e --- /dev/null +++ b/sites/kaggle/templates/competition_detail.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ c.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('competitions', c.thumbnail, c.title, 'detail-thumb') }} +
+
{{ c.category }} + {% if c.is_active %}Active + {% else %}Closed{% endif %}
+

{{ c.title }}

+
{{ c.subtitle }}
+
+ Hosted by {{ c.host }} + {{ c.reward }} prize + {{ c.num_teams|commafy }} teams +
+
+
+ {% if joined %} + ✓ Joined as {{ entry.team_name }} + {% elif c.is_active %} + Join Competition + {% else %} + Competition closed + {% endif %} +
+
+ +
+ Overview + Leaderboard + Code + {% if c.is_active %}Join{% endif %} +
+ +
+
+ {% if tab == 'leaderboard' %} +

Leaderboard

+ {% if leaderboard %} + + + + {% for s in leaderboard %} + + + + + + + + {% endfor %} + +
#TeamMembersScoreSubmitted
{% if s.rank==1 %}🥇{% elif s.rank==2 %}🥈{% elif s.rank==3 %}🥉{% else %}{{ s.rank }}{% endif %}{{ s.team_name }}{% if user_by_name(s.team_name) %}{{ s.team_name }}{% else %}team{% endif %}{{ '%.5f'|format(s.score) }}{{ s.submitted_at|reltime }}
+ {% else %}
No public submissions yet.
{% endif %} + + {% elif tab == 'code' %} +

Notebooks

+ {% if notebooks %} +
+ {% for n in notebooks %} +
+
+ +
{{ n.author_username }} · {{ n.language }}{% if n.best_score %} · score {{ n.best_score }}{% endif %}
+
+
👍 {{ n.votes|commafy }}
+
+ {% endfor %} +
+ {% else %}
No public notebooks for this competition yet.
{% endif %} + + {% elif tab == 'join' and c.is_active %} +

Join {{ c.title }}

+ {% if current_user.is_authenticated %} + {% if joined %} +

You've already joined as team {{ entry.team_name }}. Good luck!

+ {% else %} +
+ {{ join_form.csrf_token }} +
+ + {{ join_form.team_name(placeholder="Your team name") }} +
+
+ +
+ +
+ {% endif %} + {% else %} +

You must sign in to join this competition.

+ {% endif %} + + {% else %} +

Overview

+
+ {% for para in c.description.split('\n') %}

{{ para }}

{% endfor %} +
+

Evaluation

+

Submissions are scored using {{ c.metric }}.

+ {% if c.tags %}
{% for t in c.tags %}{{ t }}{% endfor %}
{% endif %} + {% endif %} +
+ + +
+
+{% endblock %} diff --git a/sites/kaggle/templates/competitions.html b/sites/kaggle/templates/competitions.html new file mode 100644 index 00000000..f9eadfbc --- /dev/null +++ b/sites/kaggle/templates/competitions.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% from "_macros.html" import competition_card %} +{% block title %}Competitions | Kaggle{% endblock %} +{% block content %} +
+

Competitions

+

Grow your data science skills by competing in our ML competitions. Find help in the docs.

+ +
+
+ + + + + +
+ {{ comps|length }} competitions +
+ + {% if comps %} +
+ {% for c in comps %}{{ competition_card(c) }}{% endfor %} +
+ {% else %} +
No competitions match your filters.
+ {% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/course_detail.html b/sites/kaggle/templates/course_detail.html new file mode 100644 index 00000000..ce55155f --- /dev/null +++ b/sites/kaggle/templates/course_detail.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}{{ course.title }} | Kaggle Learn{% endblock %} +{% block content %} +
+
+
{{ course.icon }}
+
+

{{ course.title }}

+
{{ course.level }} · {{ course.lessons }} lessons · {{ course.hours }} hours
+
{% for t in course.tags %}{{ t }}{% endfor %}
+
+ +
+
+
+

About this course

+

{{ course.description }}

+

Lessons

+
+ {% for lt in course.lesson_titles %} +
+
{{ loop.index }}. {{ lt }}
+
Tutorial + Exercise
+ {% endfor %} +
+
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/dataset_detail.html b/sites/kaggle/templates/dataset_detail.html new file mode 100644 index 00000000..500ee3e0 --- /dev/null +++ b/sites/kaggle/templates/dataset_detail.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ d.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('datasets', d.thumbnail, d.title, 'detail-thumb') }} +
+

{{ d.title }}

+
{{ d.subtitle }}
+
+ {{ d.owner_username }} + Updated {{ d.last_updated|reltime }} + Usability {{ '%.1f'|format(d.usability) }} +
+
+
+ + + ⬇ Download ({{ d.size }}) +
+
+ +
+
+

About this Dataset

+
{% for para in d.description.split('\n') %}

{{ para }}

{% endfor %}
+ {% if d.tags %}
{% for t in d.tags %}{{ t }}{% endfor %}
{% endif %} + +

Notebooks using this dataset

+ {% if notebooks %} +
+ {% for n in notebooks %} +
+ +
{{ n.author_username }} · {{ n.language }}
+
👍 {{ n.votes|commafy }}
+ {% endfor %} +
+ {% else %}

No public notebooks yet — be the first!

{% endif %} +
+ + +
+
+{% endblock %} diff --git a/sites/kaggle/templates/datasets.html b/sites/kaggle/templates/datasets.html new file mode 100644 index 00000000..fea06c1c --- /dev/null +++ b/sites/kaggle/templates/datasets.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% from "_macros.html" import dataset_card %} +{% block title %}Datasets | Kaggle{% endblock %} +{% block content %} +
+

Datasets

+

Explore, analyze, and share quality data.

+ +
+
+ + + + +
+ {{ datasets|length }} datasets +
+ + {% if tag %}

Filtered by tag {{ tag }} clear

{% endif %} + + {% if datasets %} +
{% for d in datasets %}{{ dataset_card(d) }}{% endfor %}
+ {% else %}
No datasets match your search.
{% endif %} + +
+

Popular tags

+
{% for t in all_tags %}{{ t }}{% endfor %}
+
+
+{% endblock %} diff --git a/sites/kaggle/templates/discussion_detail.html b/sites/kaggle/templates/discussion_detail.html new file mode 100644 index 00000000..f223e6aa --- /dev/null +++ b/sites/kaggle/templates/discussion_detail.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_badge %} +{% block title %}{{ d.title }} | Kaggle{% endblock %} +{% block content %} +
+

← {{ d.forum }}

+

{% if d.pinned %}📌 {% endif %}{{ d.title }}

+
+ {% if author %}{{ author.display_name }}{% else %}{{ d.author_username }}{% endif %} + {{ d.created_at|reltime }} + {{ d.forum }} +
+ + + +

{{ comments|length }} Comment{{ 's' if comments|length != 1 }}

+ {% for c in comments %} + + {% endfor %} + + {% if current_user.is_authenticated %} +
+ {{ comment_form.csrf_token }} +
{{ comment_form.body(rows=3, placeholder="Add a comment...") }}
+ +
+ {% else %} +

Sign in to join the discussion.

+ {% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/discussion_new.html b/sites/kaggle/templates/discussion_new.html new file mode 100644 index 00000000..b6b298e9 --- /dev/null +++ b/sites/kaggle/templates/discussion_new.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %}New Topic | Kaggle{% endblock %} +{% block content %} +
+

Start a new discussion

+
+ {{ form.csrf_token }} +
{{ form.title(placeholder="What's your topic?") }} + {% for e in form.title.errors %}
{{ e }}
{% endfor %}
+
{{ form.forum() }}
+
{{ form.body(rows=8, placeholder="Share details, questions, or ideas...") }} + {% for e in form.body.errors %}
{{ e }}
{% endfor %}
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/discussions.html b/sites/kaggle/templates/discussions.html new file mode 100644 index 00000000..763ce8f5 --- /dev/null +++ b/sites/kaggle/templates/discussions.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %}Discussions | Kaggle{% endblock %} +{% block content %} +
+
+

Discussions

Ask questions, share ideas, and connect with the community.

+ {% if current_user.is_authenticated %}New Topic{% endif %} +
+ +
+
+ + + + +
+ {{ discussions|length }} topics +
+ + {% if discussions %} +
+ {% for d in discussions %} +
+
+
{% if d.pinned %}📌 {% endif %}{{ d.title }}
+
{{ d.author_username }} · {{ d.forum }} · {{ d.created_at|reltime }}
+
+
👍 {{ d.votes|commafy }}
💬 {{ d.comment_count }}
+
+ {% endfor %} +
+ {% else %}
No discussions match your search.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/index.html b/sites/kaggle/templates/index.html new file mode 100644 index 00000000..e3719bc3 --- /dev/null +++ b/sites/kaggle/templates/index.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% from "_macros.html" import competition_card, dataset_card, notebook_card %} +{% block content %} +
+
+

Level up with the largest data science community

+

Kaggle is where data scientists and machine learners come together to compete, collaborate, learn, and share. Join thousands of competitions, datasets, and notebooks.

+ {% if not current_user.is_authenticated %} + Register with Email + {% else %} + Browse Competitions + {% endif %} +
+
+ +
+
+

Featured Competitions

View all →
+
+ {% for c in featured %}{{ competition_card(c) }}{% endfor %} +
+
+ +
+

Popular Datasets

View all →
+
+ {% for d in hot_datasets %}{{ dataset_card(d) }}{% endfor %} +
+
+ +
+

Trending Notebooks

View all →
+
+ {% for n in trending_notebooks %}{{ notebook_card(n) }}{% endfor %} +
+
+ +
+

Learn Data Science

All courses →
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/leaderboard.html b/sites/kaggle/templates/leaderboard.html new file mode 100644 index 00000000..b807c1cd --- /dev/null +++ b/sites/kaggle/templates/leaderboard.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}{{ c.title }} — Leaderboard | Kaggle{% endblock %} +{% block content %} +
+

← {{ c.title }}

+

Leaderboard

+

{{ c.title }} · scored by {{ c.metric }}

+ {% if leaderboard %} + + + + {% for s in leaderboard %} + + + + + + + + {% endfor %} + +
#TeamMembersScoreSubmitted
{% if s.rank==1 %}🥇{% elif s.rank==2 %}🥈{% elif s.rank==3 %}🥉{% else %}{{ s.rank }}{% endif %}{{ s.team_name }}{% if user_by_name(s.team_name) %}{{ s.team_name }}{% else %}team{% endif %}{{ '%.5f'|format(s.score) }}{{ s.submitted_at|reltime }}
+ {% else %}
No public submissions yet.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/learn.html b/sites/kaggle/templates/learn.html new file mode 100644 index 00000000..712f3a85 --- /dev/null +++ b/sites/kaggle/templates/learn.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Learn | Kaggle{% endblock %} +{% block content %} +
+

Kaggle Learn

+

Gain the skills you need to do independent data science projects. Free, hands-on, and bite-sized.

+ +
+{% endblock %} diff --git a/sites/kaggle/templates/login.html b/sites/kaggle/templates/login.html new file mode 100644 index 00000000..8bb8dc01 --- /dev/null +++ b/sites/kaggle/templates/login.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}Sign In | Kaggle{% endblock %} +{% block content %} +
+

Sign In

+

Welcome back to Kaggle

+
+ {{ form.csrf_token }} +
{{ form.email(placeholder="you@example.com") }} + {% for e in form.email.errors %}
{{ e }}
{% endfor %}
+
{{ form.password(placeholder="Password") }} + {% for e in form.password.errors %}
{{ e }}
{% endfor %}
+ +
+

New to Kaggle? Register

+
+{% endblock %} diff --git a/sites/kaggle/templates/model_detail.html b/sites/kaggle/templates/model_detail.html new file mode 100644 index 00000000..f4375295 --- /dev/null +++ b/sites/kaggle/templates/model_detail.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ m.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('models', m.thumbnail, m.title, 'detail-thumb') }} +
+

{{ m.title }}

+
+ {{ m.owner_username }} + {{ m.framework }} + {{ m.variations }} variation{{ 's' if m.variations != 1 }} + Updated {{ m.last_updated|reltime }} +
+
+
+ + +
+
+ +
+
+

Model Details

+
{% for para in m.description.split('\n') %}

{{ para }}

{% endfor %}
+ {% if m.tags %}
{% for t in m.tags %}{{ t }}{% endfor %}
{% endif %} +
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/models.html b/sites/kaggle/templates/models.html new file mode 100644 index 00000000..861b77fb --- /dev/null +++ b/sites/kaggle/templates/models.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% from "_macros.html" import model_card %} +{% block title %}Models | Kaggle{% endblock %} +{% block content %} +
+

Models

+

Use and download pre-trained models for your projects.

+ +
+
+ + + + +
+ {{ models|length }} models +
+ + {% if models %} +
{% for m in models %}{{ model_card(m) }}{% endfor %}
+ {% else %}
No models match your search.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/notebook_detail.html b/sites/kaggle/templates/notebook_detail.html new file mode 100644 index 00000000..fd079ce4 --- /dev/null +++ b/sites/kaggle/templates/notebook_detail.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ n.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('notebooks', n.thumbnail, n.title, 'detail-thumb') }} +
+

{{ n.medal_emoji }} {{ n.title }}

+
+ {{ n.author_username }} + {{ n.language }} + Last run {{ n.last_run|reltime }} + {% if n.best_score %}Best score {{ n.best_score }}{% endif %} +
+
+
+ + +
+
+ +
+
+
{% for para in n.description.split('\n') %}

{{ para }}

{% endfor %}
+ {% if n.tags %}
{% for t in n.tags %}{{ t }}{% endfor %}
{% endif %} +
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/notebooks.html b/sites/kaggle/templates/notebooks.html new file mode 100644 index 00000000..b1bf20f1 --- /dev/null +++ b/sites/kaggle/templates/notebooks.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% from "_macros.html" import notebook_card %} +{% block title %}Code | Kaggle{% endblock %} +{% block content %} +
+

Code

+

Explore and run machine learning code with Kaggle Notebooks.

+ +
+
+ + + + + +
+ {{ notebooks|length }} notebooks +
+ + {% if notebooks %} +
{% for n in notebooks %}{{ notebook_card(n) }}{% endfor %}
+ {% else %}
No notebooks match your search.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/rankings.html b/sites/kaggle/templates/rankings.html new file mode 100644 index 00000000..b59af8cb --- /dev/null +++ b/sites/kaggle/templates/rankings.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_name, medals %} +{% block title %}Rankings | Kaggle{% endblock %} +{% block content %} +
+

Rankings

+

The Progression System rewards Kagglers for their performance. See who's at the top.

+ +
+ {% for cat in categories %} + {{ cat|capitalize }} + {% endfor %} +
+ + + + + {% for u in users %} + + + + + + + + {% endfor %} + +
#UserTier ({{ category }})MedalsPoints
{{ loop.index }} + + +
{{ u.display_name }} @{{ u.username }}
+
+
{{ tier_name(u.category_tiers.get(category, u.tier)) }}{{ medals(u.gold, u.silver, u.bronze) }}{{ u.points|commafy }}
+
+{% endblock %} diff --git a/sites/kaggle/templates/register.html b/sites/kaggle/templates/register.html new file mode 100644 index 00000000..4e830618 --- /dev/null +++ b/sites/kaggle/templates/register.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block title %}Register | Kaggle{% endblock %} +{% block content %} +
+

Register

+

Join the world's largest data science community

+
+ {{ form.csrf_token }} +
{{ form.email(placeholder="you@example.com") }} + {% for e in form.email.errors %}
{{ e }}
{% endfor %}
+
{{ form.username(placeholder="username") }} + {% for e in form.username.errors %}
{{ e }}
{% endfor %}
+
{{ form.password(placeholder="At least 6 characters") }} + {% for e in form.password.errors %}
{{ e }}
{% endfor %}
+
{{ form.confirm(placeholder="Repeat password") }} + {% for e in form.confirm.errors %}
{{ e }}
{% endfor %}
+ +
+

Already have an account? Sign In

+
+{% endblock %} diff --git a/sites/kaggle/templates/search.html b/sites/kaggle/templates/search.html new file mode 100644 index 00000000..35bf13ff --- /dev/null +++ b/sites/kaggle/templates/search.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% from "_macros.html" import competition_card, dataset_card, notebook_card, model_card, tier_badge %} +{% block title %}Search: {{ q }} | Kaggle{% endblock %} +{% block content %} +
+

Search

+ {% if q %}

{{ total }} result{{ 's' if total != 1 }} for "{{ q }}"

{% else %}

Enter a query to search across Kaggle.

{% endif %} + +
+ {% for s, label in [('all','All'),('competitions','Competitions'),('datasets','Datasets'),('code','Code'),('models','Models'),('discussions','Discussions'),('users','Users')] %} + {{ label }} + {% endfor %} +
+ + {% if comps %} +

Competitions

+
{% for c in comps %}{{ competition_card(c) }}{% endfor %}
+ {% endif %} + {% if datasets %} +

Datasets

+
{% for d in datasets %}{{ dataset_card(d) }}{% endfor %}
+ {% endif %} + {% if notebooks %} +

Code

+
{% for n in notebooks %}{{ notebook_card(n) }}{% endfor %}
+ {% endif %} + {% if models %} +

Models

+
{% for m in models %}{{ model_card(m) }}{% endfor %}
+ {% endif %} + {% if discussions %} +

Discussions

+
{% for d in discussions %} +
+
{{ d.author_username }} · {{ d.forum }}
👍 {{ d.votes }}
+ {% endfor %}
+ {% endif %} + {% if users %} +

Users

+
{% for u in users %} +
+
{{ u.display_name }} {{ tier_badge(u) }}
+
@{{ u.username }} · {{ u.occupation }}
{{ u.points|commafy }} pts
+ {% endfor %}
+ {% endif %} + + {% if q and total == 0 %}
No results found. Try different keywords.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/user_profile.html b/sites/kaggle/templates/user_profile.html new file mode 100644 index 00000000..bd41bb92 --- /dev/null +++ b/sites/kaggle/templates/user_profile.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_badge, medals, dataset_card, notebook_card, model_card, competition_card %} +{% block title %}{{ u.display_name }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ u.username }} +
+

{{ u.display_name }} {{ tier_badge(u) }}

+
@{{ u.username }}{% if u.comp_rank %} · Competitions rank #{{ u.comp_rank }}{% endif %}
+ {% if u.occupation or u.organization %}

{{ u.occupation }}{% if u.organization %} at {{ u.organization }}{% endif %}

{% endif %} + {% if u.location %}

📍 {{ u.location }}

{% endif %} + {% if u.bio %}

{{ u.bio }}

{% endif %} +
{{ medals(u.gold, u.silver, u.bronze) }} · {{ u.points|commafy }} points · {{ follower_count }} followers
+
+
+ {% if current_user.is_authenticated and current_user.username != u.username %} + + {% endif %} +
+
+ +
+ Tiers — {% for cat, t in u.category_tiers.items() %}{{ cat|capitalize }}: {{ t }}{% if not loop.last %} · {% endif %}{% endfor %} +
+ + {% if hosted %} +

Hosted Competitions

+
{% for c in hosted %}{{ competition_card(c) }}{% endfor %}
+ {% endif %} + {% if datasets %} +

Datasets ({{ datasets|length }})

+
{% for d in datasets %}{{ dataset_card(d) }}{% endfor %}
+ {% endif %} + {% if notebooks %} +

Notebooks ({{ notebooks|length }})

+
{% for n in notebooks %}{{ notebook_card(n) }}{% endfor %}
+ {% endif %} + {% if models %} +

Models ({{ models|length }})

+
{% for m in models %}{{ model_card(m) }}{% endfor %}
+ {% endif %} + {% if discussions %} +

Discussions ({{ discussions|length }})

+
{% for d in discussions %} +
+
{{ d.forum }}
👍 {{ d.votes }}
+ {% endfor %}
+ {% endif %} + {% if not (hosted or datasets or notebooks or models or discussions) %} +
{{ u.display_name }} hasn't published any public work yet.
+ {% endif %} +
+{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index 72defad8..7d6ce175 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,7 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn) + cambridge_dictionary coursera espn kaggle) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR"