diff --git a/.assets-revision b/.assets-revision index 29bb8fc6..a25e5b8b 100644 --- a/.assets-revision +++ b/.assets-revision @@ -5,4 +5,4 @@ # is a git revision (branch name like `main`, a tag, or a specific commit # sha). Override at runtime with the ASSETS_REVISION env var. repo: ChilleD/WebHarbor -revision: 480c892e976bada6c0ea3f5a66e2b9efda65525d +revision: db9c73e62d853ed91f6152b5b7105571502b7e07 diff --git a/AGENTS.md b/AGENTS.md index 14e45939..f38f767b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ A coding agent (Claude Code, Cursor, Aider, Codex, ...) is reading this. Read on ## What it is -20 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. +21 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. Two repos: - **code** (this one) — Flask apps, control plane, scripts. @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn//`. The path predates the ren # fresh clone ./scripts/fetch_assets.sh # pulls assets from HF ./scripts/build.sh # docker build -t webharbor:dev . -docker run -d -p 8101:8101 -p 40000-40019:40000-40019 webharbor:dev +docker run -d -p 8101:8101 -p 40000-40020:40000-40020 webharbor:dev ``` Or use the published image directly: ```bash -docker run -d -p 8101:8101 -p 40000-40019:40000-40019 \ +docker run -d -p 8101:8101 -p 40000-40020:40000-40020 \ battalion7244/webharbor:latest ``` -Sites are on `40000`-`40019` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: +Sites are on `40000`-`40020` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: | Method | Path | Purpose | |--------|---------------------|-------------------------------------------| @@ -136,13 +136,13 @@ python3 -m py_compile sites//app.py # 3. run on alt ports (don't collide with anything you already have running) docker run -d --rm --name wh-test \ - -p 8201:8101 -p 41000-41019:40000-40019 webharbor:dev + -p 8201:8101 -p 41000-41020:40000-40020 webharbor:dev # 4. control plane healthy, all sites alive curl -s http://localhost:8201/health | python3 -m json.tool | head # 5. every site renders 200 -for p in $(seq 41000 41019); do +for p in $(seq 41000 41020); do curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/ done diff --git a/CLAUDE.md b/CLAUDE.md index 5dcf2283..029a8238 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl ## Existing containers -If a container is already running on `:8101` / `:40000-40019`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41019`). +If a container is already running on `:8101` / `:40000-40020`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41020`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6dce56be..7a838f34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ git clone https://github.com//webharbor && cd webharbor ./scripts/fetch_assets.sh # pull current assets ./scripts/new_site.py mywebsite # OR edit an existing site ./scripts/build.sh && docker run -d --rm \ - -p 8101:8101 -p 40000-40019:40000-40019 webharbor:dev + -p 8101:8101 -p 40000-40020:40000-40020 webharbor:dev # iterate locally... ./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out diff --git a/Dockerfile b/Dockerfile index 51f61ce7..64b55fa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 20 Flask mirror sites + control plane on :8101. +# 21 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -41,6 +41,16 @@ 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-40019 +# OSU's real-site image bundle is required, while its database is generated +# deterministically from tracked source data. +RUN test -n "$(ls -A /opt/WebSyn/osu/static/images)" +RUN cd /opt/WebSyn/osu && python3 -c "\ +import app; \ +import os, shutil; \ +os.makedirs('instance_seed', exist_ok=True); \ +shutil.copy2('instance/osu.db', 'instance_seed/osu.db'); \ +print('osu seed DB generated at build time.')" && rm -rf /opt/WebSyn/osu/instance + +EXPOSE 8101 40000-40020 CMD ["/opt/websyn_start.sh"] diff --git a/README.md b/README.md index 28457c4e..ea2c454a 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod - **Deep features unlocked** — carts, checkouts, accounts, all fully testable - **Evolving** — harder tasks drive richer mirrors; the environment grows with agents - **RL-ready** — sub-second database resets between rollouts -- **Community-driven** — 20 sites today, scaling to 100+ together +- **Community-driven** — 21 sites today, scaling to 100+ together ## 🚀 Quickstart One command to run all web environments: ```bash -docker run -p 8101:8101 -p 40000-40019:40000-40019 battalion7244/webharbor:latest +docker run -p 8101:8101 -p 40000-40020:40000-40020 battalion7244/webharbor:latest ``` -Then point your agent at `http://localhost:40000` through `http://localhost:40019` to explore 20 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, and TED`. +Then point your agent at `http://localhost:40000` through `http://localhost:40020` to explore 21 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, TED, and Ohio State University`. For sub-second reset between rollouts, expose the control plane and call `/reset/`: @@ -65,7 +65,7 @@ git clone https://github.com/aiming-lab/WebHarbor && cd WebHarbor ## 🤝 Contribute -We have built 20 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. +We have built 21 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. There are two ways to join the author list: diff --git a/agent_demo/README.md b/agent_demo/README.md index a8a1a5c2..da9d03f8 100644 --- a/agent_demo/README.md +++ b/agent_demo/README.md @@ -19,7 +19,7 @@ export OPENAI_BASE_URL=https://api.openai.com/v1 # or your Azure / vLLM endpoi ## Run a task -WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40019:40000-40019 battalion7244/webharbor:latest`). +WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40020:40000-40020 battalion7244/webharbor:latest`). Run a single task from a site's `tasks.jsonl`: diff --git a/control_server.py b/control_server.py index bda229c5..be967923 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', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', + 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', 'osu', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/review-reports/PR-63-FINAL-AUDIT.md b/review-reports/PR-63-FINAL-AUDIT.md new file mode 100644 index 00000000..fa5635eb --- /dev/null +++ b/review-reports/PR-63-FINAL-AUDIT.md @@ -0,0 +1,40 @@ +# PR #63 independent review and remediation audit + +## Scope + +Seven independent review contexts examined PR #63 head `4890c86374daea2fa29455b919b0ab55095efc3d` against its original base `438a029c04d86b22c710ad5d985d1d1491d2cb98`. Integration was also checked against current main `e911a6adb28d04397ed30d8293e77b0c62a112d5`. Raw reports are retained outside the repository under `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-agents/reports/`. + +## Agent findings and dispositions + +| Review agent | Findings on original PR head | Verification and disposition | +|---|---|---| +| Security and state | Hard-coded secret, login/bookmark open redirects, GET logout, news GET writes, unconstrained/dangling bookmarks, no bookmark uniqueness constraint, malformed session crash, weak username normalization, and unbounded notes. | Confirmed in `sites/osu/app.py`. Fixed with environment/random secret, local redirect validation, POST logout, read-only news GET, bookmark type/object validation, 500-character notes, a database unique constraint with race handling, robust user loading, lowercase usernames, request limits, session rotation, and SQLite foreign keys. | +| Task and data | Task 1 conflicted with the number of seeded team rows; several tasks were answerable from prior knowledge; task 9 accepted incomplete degree types; task 17 did not ask for a precise output; many valid-answer aliases increased grading ambiguity. | Rewrote all 20 tasks around explicit visible routes, filters, multi-field facts, and comparisons. Task 1 now explicitly asks for the About-page display. Task 9 requires all three Engineering degree types. Task 17 requires exact article title and author. Multi-entity tasks bind each number/value to its entity. | +| Verifier robustness | URL substring checks accepted external or wrong paths; required search/filter/topic steps were missing; negated answers and unrelated numbers could pass; `--no_llm` removed semantic enforcement; the synthetic selfcheck omitted adversarial cases. | Replaced the common verifier library and all 20 verifiers with same-origin exact path/query/order checks, visible-link transition checks, exact task IDs, non-empty output, negation-aware facts, entity-bound comparison values, and complete database equality for every read-only task. Added positive and adversarial tests for wrong task IDs, answer-only runs, external origins, database mutation, missing filters, negation, and swapped values. | +| UI and responsive behavior | Fixed two-column detail layouts overflowed on mobile; header/search/top bar could overflow; navigation lacked an overflow affordance and current-page semantics; forms lacked clear labels; focus styling was incomplete; cards with inline flex layouts could overflow; the mirror contained no real photographic assets. | Added responsive detail layout classes, mobile header stacking, horizontally scrollable navigation, `min-width:0` and wrapping protections, fixed grid breakpoint ordering, accessible search names, current-page `aria-current`, and focus-visible styles. Crawled 19 photographs from official Ohio State web properties, preserved source-page URLs and hashes in `sites/osu/image_sources.json`, and integrated the photographs into home, section, card, and detail layouts. Automated tests cover 320 px, 390 px, and 1440 px layouts. | +| Integration | The PR was based on a 17-site tree while current main has 20 sites; OSU collided with IKEA at `40016`; docs and asset pins were stale; clean `scripts/build.sh` would reject OSU because it intentionally has no HF seed bundle. | Merged current main locally, retained all existing sites, appended OSU as site index 20 on `40020`, exposed `40000-40020`, and updated all documentation. Added `.build-generated-seed` support for the deterministic OSU database and `.requires-images` enforcement for its HF-hosted image bundle. The pinned HF revision now contains `osu.tar.gz`. | +| Application and data model | Import-time seed recursion under `python app.py`, broad partial-state seed gate, wall-clock event filtering, nondeterministic seeded user hashes/timestamps, missing bookmark referential validation, and brittle phrase search. | Added module-safe seed resolution, partial-database failure, fixed benchmark time, stable seeded password hash and timestamps, exact bookmark validation, and token-overlap ranked global search. The same source now generates byte-identical OSU databases in repeated clean runs. | +| Test evidence | The original selfcheck used fabricated trajectories only; it did not run the site, verify rendered facts, test external-origin/path spoofing, detect database mutations, validate responsive pages, or prove a current-main Docker build. | Replaced the original selfcheck with a complete unittest entry point and added HTTP, seed, integration, positive verifier, and adversarial verifier suites. Actual Playwright task trajectories, screenshots, and before/after databases were produced for all 20 tasks. A clean build without a host OSU seed and a 21-site container smoke/reset test were completed. | + +## Validation + +- Python compilation, shell syntax, Ruff fatal/undefined-name checks, and `git diff --check`: PASS. +- OSU HTTP, seed, image provenance, integration, and verifier suite: 24 tests PASS. +- Actual Playwright completion from fresh databases: 20/20 PASS. +- Deterministic verifier results for those browser runs: 20/20 PASS. +- Responsive checks at 320 px, 390 px, and 1440 px: 45/45 PASS with no page-level horizontal overflow. +- Repeated clean OSU seed generation: byte-identical. +- Official-image provenance manifest: 19/19 assets present, non-empty, and SHA-256 verified. +- Hugging Face dataset PR #56 merged; pinned revision `db9c73e62d853ed91f6152b5b7105571502b7e07` contains `osu.tar.gz`. +- Pinned HF `osu.tar.gz` download, deterministic tar SHA-256 (`1fc684a25890262137714b56577cd8bcbe0c5a867c65da12f3d45ab7662949f7`), and extraction: PASS. +- Clean Docker build with `sites/osu/instance_seed` absent from the build context: PASS. +- Container health: all 21 sites alive and all 21 site roots returned HTTP 200. +- `/reset/osu`: PASS; runtime and seed SHA-256 values match. +- `/reset-all`: PASS for all 21 sites. + +## Evidence + +- Agent reports: `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-agents/reports/` +- Browser trajectories, screenshots, and verifier outputs: `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-fixes/e2e/` +- Responsive results and screenshots: `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr63-fixes/responsive/` +- Official image source pages, source URLs, dimensions, and hashes: `sites/osu/image_sources.json` diff --git a/scripts/build.sh b/scripts/build.sh index cff28b95..1953c7a6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -10,6 +10,13 @@ TAG="${1:-webharbor:dev}" # Fast probe — if any site is missing instance_seed/, run fetch_assets. need_fetch=0 for site in sites/*/; do + if [[ -f "${site}.requires-images" ]] && { [[ ! -d "${site}static/images" ]] || [[ -z $(ls -A "${site}static/images" 2>/dev/null) ]]; }; then + need_fetch=1 + break + fi + if [[ -f "${site}.build-generated-seed" ]]; then + continue + fi if [[ ! -d "${site}instance_seed" ]]; then need_fetch=1 break diff --git a/scripts/check_assets.sh b/scripts/check_assets.sh index 61e3e7bc..2ced805d 100755 --- a/scripts/check_assets.sh +++ b/scripts/check_assets.sh @@ -18,13 +18,19 @@ warnings=0 for site in sites/*/; do s=$(basename "$site") for sub in "${REQUIRED[@]}"; do + if [[ -f "sites/$s/.build-generated-seed" && "$sub" == "instance_seed" ]]; then + continue + fi if [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; then echo " MISSING (required): sites/$s/$sub" missing=$((missing + 1)) fi done for sub in "${OPTIONAL[@]}"; do - if [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; then + if [[ -f "sites/$s/.requires-images" && "$sub" == "static/images" ]] && { [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; }; then + echo " MISSING (required): sites/$s/$sub" + missing=$((missing + 1)) + elif [[ ! -d "sites/$s/$sub" ]] || [[ -z $(ls -A "sites/$s/$sub" 2>/dev/null) ]]; then warnings=$((warnings + 1)) fi done diff --git a/scripts/extract_assets.sh b/scripts/extract_assets.sh index 1dd5e8b0..14fbe3a6 100755 --- a/scripts/extract_assets.sh +++ b/scripts/extract_assets.sh @@ -35,6 +35,9 @@ for site_dir in sites/*/; do members=() for sub in "${SUBPATHS[@]}"; do + if [[ "$sub" == "instance_seed" && -f "${site_dir}.build-generated-seed" ]]; then + continue + fi [[ -e "$site_dir$sub" ]] && members+=("$site/$sub") done if [[ ${#members[@]} -eq 0 ]]; then diff --git a/sites/osu/.build-generated-seed b/sites/osu/.build-generated-seed new file mode 100644 index 00000000..469d2bdb --- /dev/null +++ b/sites/osu/.build-generated-seed @@ -0,0 +1 @@ +The Dockerfile generates instance_seed/osu.db deterministically from tracked source data. diff --git a/sites/osu/.requires-images b/sites/osu/.requires-images new file mode 100644 index 00000000..7c9b0bb4 --- /dev/null +++ b/sites/osu/.requires-images @@ -0,0 +1 @@ +This site requires the static/images directory supplied by the pinned Hugging Face asset bundle. diff --git a/sites/osu/_health.py b/sites/osu/_health.py new file mode 100644 index 00000000..0d868f36 --- /dev/null +++ b/sites/osu/_health.py @@ -0,0 +1,17 @@ +"""Health check module for OSU mirror site.""" + + +def health_check(app, db, College, Program): + """Return health status dict.""" + try: + with app.app_context(): + college_count = College.query.count() + program_count = Program.query.count() + return { + 'ok': True, + 'site': 'osu', + 'colleges': college_count, + 'programs': program_count, + } + except Exception as e: + return {'ok': False, 'error': str(e)} diff --git a/sites/osu/app.py b/sites/osu/app.py new file mode 100644 index 00000000..d9b13f94 --- /dev/null +++ b/sites/osu/app.py @@ -0,0 +1,922 @@ +#!/usr/bin/env python3 +"""Ohio State University mirror — Flask application.""" +import json +import os +import re +import secrets +from datetime import datetime, timezone +from math import ceil + +from flask import (Flask, render_template, request, redirect, url_for, + flash, jsonify, session, abort) +from flask_sqlalchemy import SQLAlchemy +from flask_login import (LoginManager, UserMixin, login_user, logout_user, + login_required, current_user) +from flask_wtf import FlaskForm +from flask_wtf.csrf import CSRFProtect +from flask_bcrypt import Bcrypt +from sqlalchemy import event as sqlalchemy_event +from sqlalchemy.engine import Engine +from sqlalchemy.exc import IntegrityError +from wtforms import StringField, PasswordField +from wtforms.validators import DataRequired, Email, Length, EqualTo + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +SITE_PORT = 40020 +BENCHMARK_NOW = datetime(2024, 10, 15, 12, 0, 0) +with open(os.path.join(BASE_DIR, 'image_sources.json'), encoding='utf-8') as image_manifest_file: + IMAGE_ASSETS = {item['file'].removesuffix('.webp'): item for item in json.load(image_manifest_file)['images']} + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.environ.get('OSU_SECRET_KEY') or secrets.token_hex(32) +app.config['SQLALCHEMY_DATABASE_URI'] = ( + f"sqlite:///{os.path.join(BASE_DIR, 'instance', 'osu.db')}") +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['WTF_CSRF_TIME_LIMIT'] = None +app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 +app.config['SESSION_COOKIE_HTTPONLY'] = True +app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' + +os.makedirs(os.path.join(BASE_DIR, 'instance'), exist_ok=True) + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) + + +@sqlalchemy_event.listens_for(Engine, 'connect') +def enable_sqlite_foreign_keys(connection, _record): + cursor = connection.cursor() + cursor.execute('PRAGMA foreign_keys=ON') + cursor.close() + + +login_manager = LoginManager(app) +login_manager.login_view = 'login' +login_manager.login_message = 'Please sign in to continue.' +login_manager.login_message_category = 'info' +csrf = CSRFProtect(app) + +PER_PAGE = 20 + +# ─── Helpers ────────────────────────────────────────────────────────────────── + +def utcnow(): + return datetime.now(timezone.utc).replace(tzinfo=None) + + +def slugify(text): + if not text: + return '' + s = re.sub(r'[^a-zA-Z0-9\s-]', '', text) + s = re.sub(r'[\s]+', '-', s.strip().lower()) + return s + + +def safe_next(target, fallback): + if not target or '\\' in target or not target.startswith('/') or target.startswith('//'): + return fallback + return target + + +def search_tokens(value): + return [token for token in re.split(r'[^a-z0-9]+', (value or '').casefold()) if len(token) > 1] + + +def ranked_search(rows, fields, query, limit=10): + tokens = search_tokens(query) + ranked = [] + for row in rows: + text = ' '.join(str(getattr(row, field, '') or '') for field in fields).casefold() + score = sum(1 for token in tokens if token in text) + if score: + ranked.append((score, row.id, row)) + return [row for _score, _row_id, row in sorted(ranked, key=lambda item: (-item[0], item[1]))[:limit]] + + +def image_asset(key): + return IMAGE_ASSETS[key] + + +def news_image(article): + if article.slug == 'ohio-state-researchers-develop-breakthrough-cancer-immunotherapy': + return image_asset('cancer-immunotherapy') + if article.slug == 'ohio-state-sets-record-for-research-expenditures-at-13-billion': + return image_asset('research-hero') + category_images = { + 'Athletics': 'athletics-football', + 'Health': 'about-health-care', + 'Research': 'research-microelectronics', + 'Student': 'academics-graduate', + 'Faculty': 'about-education', + 'Campus Life': 'campus-life', + } + return image_asset(category_images.get(article.category, 'news-campus')) + + +def research_image(center): + center_images = { + 'translational-data-analytics-institute': 'research-hero', + 'james-cancer-hospital-and-solove-research-institute': 'james-cancer-hospital', + 'center-for-clean-hydrogen': 'research-mobility', + 'ohio-supercomputer-center': 'research-microelectronics', + } + return image_asset(center_images.get(center.slug, 'research-hero')) + + +def college_image(college): + college_images = { + 'arts-and-sciences': 'academics-undergraduate', + 'fisher-college-of-business': 'fisher-students', + 'education-and-human-ecology': 'about-education', + 'engineering': 'research-microelectronics', + 'food-agricultural-and-environmental-sciences': 'research-mobility', + 'moritz-college-of-law': 'campus-life', + 'medicine': 'about-health-care', + 'nursing': 'about-health-care', + 'optometry': 'academics-online', + 'pharmacy': 'research-hero', + 'public-health': 'about-health-care', + 'social-work': 'campus-life', + 'veterinary-medicine': 'research-hero', + 'john-glenn-college-of-public-affairs': 'home-hero', + 'dentistry': 'about-health-care', + 'graduate-school': 'academics-graduate', + } + return image_asset(college_images.get(college.slug, 'academics-undergraduate')) + + +def athletics_image(team): + team_images = { + 'ohio-state-buckeyes-football': 'athletics-football', + 'ohio-state-buckeyes-mens-basketball': 'athletics-basketball', + 'ohio-state-buckeyes-wrestling': 'athletics-wrestling', + 'ohio-state-buckeyes-fencing': 'athletics-fencing', + } + key = team_images.get(team.slug) + return image_asset(key) if key else None + +# ─── Models ─────────────────────────────────────────────────────────────────── + +class User(db.Model, UserMixin): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False, index=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + full_name = db.Column(db.String(150), nullable=False, default='') + role = db.Column(db.String(30), default='student') + bio = db.Column(db.Text, default='') + created_at = db.Column(db.DateTime, default=utcnow) + + bookmarks = db.relationship('Bookmark', backref='user', lazy=True, + cascade='all, delete-orphan') + + def set_password(self, pw): + self.password_hash = bcrypt.generate_password_hash(pw).decode('utf-8') + + def check_password(self, pw): + return bcrypt.check_password_hash(self.password_hash, pw) + + +class College(db.Model): + __tablename__ = 'colleges' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + description = db.Column(db.Text, default='') + dean = db.Column(db.String(150), default='') + founded_year = db.Column(db.Integer, default=1870) + undergrad_count = db.Column(db.Integer, default=1000) + grad_count = db.Column(db.Integer, default=500) + campus = db.Column(db.String(100), default='Columbus') + + departments = db.relationship('Department', backref='college', lazy=True) + programs = db.relationship('Program', backref='college', lazy=True) + research_centers = db.relationship('ResearchCenter', backref='college', lazy=True) + + +class Department(db.Model): + __tablename__ = 'departments' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + college_id = db.Column(db.Integer, db.ForeignKey('colleges.id'), nullable=False) + description = db.Column(db.Text, default='') + chair = db.Column(db.String(150), default='') + phone = db.Column(db.String(30), default='') + location = db.Column(db.String(200), default='') + + faculty = db.relationship('Faculty', backref='department', lazy=True) + programs = db.relationship('Program', backref='department', lazy=True) + + +class Program(db.Model): + __tablename__ = 'programs' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(300), nullable=False) + slug = db.Column(db.String(300), unique=True, nullable=False, index=True) + degree_type = db.Column(db.String(20), default='BA') + college_id = db.Column(db.Integer, db.ForeignKey('colleges.id'), nullable=True) + department_id = db.Column(db.Integer, db.ForeignKey('departments.id'), nullable=True) + description = db.Column(db.Text, default='') + requirements = db.Column(db.Text, default='') + units = db.Column(db.Integer, default=120) + duration_years = db.Column(db.Float, default=4.0) + application_deadline = db.Column(db.String(80), default='') + is_online = db.Column(db.Boolean, default=False) + gre_required = db.Column(db.Boolean, default=False) + + +class NewsArticle(db.Model): + __tablename__ = 'news_articles' + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(300), nullable=False) + slug = db.Column(db.String(300), unique=True, nullable=False, index=True) + category = db.Column(db.String(50), default='Campus Life') + author = db.Column(db.String(150), default='OSU News Staff') + published_date = db.Column(db.DateTime, default=utcnow) + content = db.Column(db.Text, default='') + summary = db.Column(db.Text, default='') + tags = db.Column(db.String(500), default='') + view_count = db.Column(db.Integer, default=0) + featured = db.Column(db.Boolean, default=False) + + +class Event(db.Model): + __tablename__ = 'events' + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(300), nullable=False) + description = db.Column(db.Text, default='') + start_datetime = db.Column(db.DateTime, nullable=False) + end_datetime = db.Column(db.DateTime, nullable=True) + location = db.Column(db.String(300), default='') + building = db.Column(db.String(200), default='') + campus = db.Column(db.String(100), default='Columbus') + category = db.Column(db.String(50), default='Lecture') + organizer = db.Column(db.String(200), default='') + registration_required = db.Column(db.Boolean, default=False) + cost = db.Column(db.String(50), default='Free') + + +class ResearchCenter(db.Model): + __tablename__ = 'research_centers' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(300), nullable=False) + slug = db.Column(db.String(300), unique=True, nullable=False, index=True) + description = db.Column(db.Text, default='') + director = db.Column(db.String(150), default='') + college_id = db.Column(db.Integer, db.ForeignKey('colleges.id'), nullable=True) + focus_areas = db.Column(db.String(500), default='') + url = db.Column(db.String(300), default='') + founded_year = db.Column(db.Integer, default=2000) + + +class Faculty(db.Model): + __tablename__ = 'faculty' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(150), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), default='Professor') + department_id = db.Column(db.Integer, db.ForeignKey('departments.id'), nullable=True) + email = db.Column(db.String(120), default='') + office = db.Column(db.String(200), default='') + phone = db.Column(db.String(30), default='') + research_interests = db.Column(db.String(500), default='') + bio = db.Column(db.Text, default='') + is_emeritus = db.Column(db.Boolean, default=False) + + +class AthleticTeam(db.Model): + __tablename__ = 'athletic_teams' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + sport = db.Column(db.String(100), nullable=False) + gender = db.Column(db.String(20), default='Men') + conference = db.Column(db.String(100), default='Big Ten') + coach = db.Column(db.String(150), default='') + home_venue = db.Column(db.String(200), default='') + national_titles = db.Column(db.Integer, default=0) + recent_record = db.Column(db.String(50), default='') + + +class Bookmark(db.Model): + __tablename__ = 'bookmarks' + __table_args__ = (db.UniqueConstraint('user_id', 'item_type', 'item_id', name='uq_bookmark_user_item'),) + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + item_type = db.Column(db.String(50), nullable=False) + item_id = db.Column(db.Integer, nullable=False) + note = db.Column(db.Text, default='') + created_at = db.Column(db.DateTime, default=utcnow) + + +BOOKMARK_TARGETS = { + 'program': Program, + 'news': NewsArticle, + 'event': Event, + 'faculty': Faculty, + 'research': ResearchCenter, + 'athletics': AthleticTeam, +} + + +# ─── Forms ──────────────────────────────────────────────────────────────────── + +class LoginForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email(), Length(max=120)]) + password = PasswordField('Password', validators=[DataRequired(), Length(max=100)]) + +class RegisterForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), Length(3, 80)]) + full_name = StringField('Full Name', validators=[DataRequired(), Length(2, 150)]) + email = StringField('Email', validators=[DataRequired(), Email(), Length(max=120)]) + password = PasswordField('Password', validators=[DataRequired(), Length(8, 100)]) + confirm = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) + +# ─── Login Manager ──────────────────────────────────────────────────────────── + +@login_manager.user_loader +def load_user(user_id): + try: + return db.session.get(User, int(user_id)) + except (TypeError, ValueError): + return None + +# ─── Context Processors ─────────────────────────────────────────────────────── + +@app.context_processor +def inject_globals(): + return { + 'now': BENCHMARK_NOW, + 'colleges': College.query.order_by(College.name).all(), + 'image_asset': image_asset, + 'news_image': news_image, + 'research_image': research_image, + 'college_image': college_image, + 'athletics_image': athletics_image, + } + +# ─── Routes ─────────────────────────────────────────────────────────────────── + +@app.route('/') +def index(): + featured_news = NewsArticle.query.filter_by(featured=True).order_by( + NewsArticle.published_date.desc()).limit(6).all() + if len(featured_news) < 3: + featured_news = NewsArticle.query.order_by( + NewsArticle.published_date.desc()).limit(6).all() + upcoming_events = Event.query.filter( + Event.start_datetime >= BENCHMARK_NOW + ).order_by(Event.start_datetime).limit(4).all() + recent_research = ResearchCenter.query.limit(4).all() + stats = { + 'fulbright_rank': 1, + 'undergrad_majors': 200, + 'grad_programs': 278, + 'varsity_sports': 36, + 'faculty_count': 7000, + 'undergrad_count': 46820, + 'grad_count': 14000, + 'degree_programs': 500, + 'buckeython_raised': 13, + 'extension_offices': 88, + 'campuses': 6, + 'research_expenditure': 1.3, + } + return render_template('index.html', + featured_news=featured_news, + upcoming_events=upcoming_events, + recent_research=recent_research, + stats=stats) + + +@app.route('/news') +def news(): + q = request.args.get('q', '').strip() + category = request.args.get('category', '') + featured = request.args.get('featured', '') + page = max(1, request.args.get('page', 1, type=int) or 1) + + query = NewsArticle.query + if q: + query = query.filter( + db.or_( + NewsArticle.title.ilike(f'%{q}%'), + NewsArticle.summary.ilike(f'%{q}%'), + NewsArticle.content.ilike(f'%{q}%'), + NewsArticle.tags.ilike(f'%{q}%'), + )) + if category: + query = query.filter(NewsArticle.category == category) + if featured == '1': + query = query.filter(NewsArticle.featured == True) + + query = query.order_by(NewsArticle.published_date.desc()) + total = query.count() + articles = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + categories = ['Research', 'Campus Life', 'Faculty', 'Student', 'Athletics', + 'Science', 'Health'] + return render_template('news.html', + articles=articles, + total=total, + page=page, + total_pages=total_pages, + categories=categories, + current_category=category, + q=q, + featured=featured) + + +@app.route('/news/') +def news_article(slug): + article = NewsArticle.query.filter_by(slug=slug).first_or_404() + related = NewsArticle.query.filter( + NewsArticle.category == article.category, + NewsArticle.id != article.id + ).order_by(NewsArticle.published_date.desc()).limit(3).all() + return render_template('news_article.html', article=article, related=related) + + +@app.route('/academics') +def academics(): + colleges = College.query.order_by(College.name).all() + total_programs = Program.query.count() + total_depts = Department.query.count() + return render_template('academics.html', + colleges=colleges, + total_programs=total_programs, + total_depts=total_depts) + + +@app.route('/programs') +def programs(): + q = request.args.get('q', '').strip() + college_slug = request.args.get('college', '') + degree = request.args.get('degree', '') + online = request.args.get('online', '') + page = max(1, request.args.get('page', 1, type=int) or 1) + + query = Program.query + if q: + query = query.filter( + db.or_( + Program.name.ilike(f'%{q}%'), + Program.description.ilike(f'%{q}%'), + )) + if college_slug: + col = College.query.filter_by(slug=college_slug).first() + if col: + query = query.filter(Program.college_id == col.id) + if degree: + query = query.filter(Program.degree_type == degree) + if online == '1': + query = query.filter(Program.is_online == True) + + query = query.order_by(Program.name) + total = query.count() + progs = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + all_colleges = College.query.order_by(College.name).all() + degree_types = ['BA', 'BS', 'MA', 'MS', 'PhD', 'MPH', 'MBA', 'JD', 'MD', + 'PharmD', 'DVM', 'OD'] + return render_template('programs.html', + programs=progs, + total=total, + page=page, + total_pages=total_pages, + all_colleges=all_colleges, + degree_types=degree_types, + current_college=college_slug, + current_degree=degree, + online=online, + q=q) + + +@app.route('/programs/') +def program_detail(slug): + program = Program.query.filter_by(slug=slug).first_or_404() + related = Program.query.filter( + Program.college_id == program.college_id, + Program.id != program.id + ).limit(4).all() + return render_template('program_detail.html', program=program, related=related) + + +@app.route('/events') +def events(): + q = request.args.get('q', '').strip() + category = request.args.get('category', '') + campus = request.args.get('campus', '') + date_filter = request.args.get('date', 'upcoming') + page = max(1, request.args.get('page', 1, type=int) or 1) + now = BENCHMARK_NOW + + query = Event.query + if q: + query = query.filter( + db.or_( + Event.title.ilike(f'%{q}%'), + Event.description.ilike(f'%{q}%'), + Event.location.ilike(f'%{q}%'), + Event.organizer.ilike(f'%{q}%'), + )) + if category: + query = query.filter(Event.category == category) + if campus: + query = query.filter(Event.campus == campus) + if date_filter == 'upcoming': + query = query.filter(Event.start_datetime >= now) + query = query.order_by(Event.start_datetime) + elif date_filter == 'past': + query = query.filter(Event.start_datetime < now) + query = query.order_by(Event.start_datetime.desc()) + elif date_filter == 'today': + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + today_end = now.replace(hour=23, minute=59, second=59) + query = query.filter(Event.start_datetime.between(today_start, today_end)) + query = query.order_by(Event.start_datetime) + else: + query = query.order_by(Event.start_datetime) + + total = query.count() + evts = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + categories = ['Lecture', 'Sports', 'Arts', 'Career', 'Health', 'Social', 'Virtual'] + campuses = [row[0] for row in db.session.query(Event.campus).distinct().order_by(Event.campus).all()] + return render_template('events.html', + events=evts, + total=total, + page=page, + total_pages=total_pages, + categories=categories, + campuses=campuses, + current_category=category, + current_campus=campus, + date_filter=date_filter, + q=q) + + +@app.route('/events/') +def event_detail(event_id): + event = db.session.get(Event, event_id) + if event is None: + abort(404) + related = Event.query.filter( + Event.category == event.category, + Event.id != event.id, + Event.start_datetime >= BENCHMARK_NOW + ).order_by(Event.start_datetime).limit(3).all() + return render_template('event_detail.html', event=event, related=related) + + +@app.route('/research') +def research(): + centers = ResearchCenter.query.order_by(ResearchCenter.name).all() + colleges = College.query.order_by(College.name).all() + return render_template('research.html', centers=centers, colleges=colleges) + + +@app.route('/research/') +def research_center(slug): + center = ResearchCenter.query.filter_by(slug=slug).first_or_404() + related = ResearchCenter.query.filter( + ResearchCenter.college_id == center.college_id, + ResearchCenter.id != center.id + ).limit(3).all() + return render_template('research_center.html', center=center, related=related) + + +@app.route('/departments') +def departments(): + all_colleges = College.query.order_by(College.name).all() + depts_by_college = {} + for college in all_colleges: + depts_by_college[college] = Department.query.filter_by( + college_id=college.id).order_by(Department.name).all() + return render_template('departments.html', depts_by_college=depts_by_college) + + +@app.route('/departments/') +def department_detail(slug): + dept = Department.query.filter_by(slug=slug).first_or_404() + faculty_list = Faculty.query.filter_by(department_id=dept.id).order_by(Faculty.name).all() + dept_programs = Program.query.filter_by(department_id=dept.id).all() + return render_template('department_detail.html', + dept=dept, + faculty_list=faculty_list, + programs=dept_programs) + + +@app.route('/admissions') +def admissions(): + undergrad_programs = Program.query.filter( + Program.degree_type.in_(['BA', 'BS']) + ).count() + grad_programs = Program.query.filter( + Program.degree_type.in_(['MA', 'MS', 'PhD', 'MPH', 'MBA', 'JD', 'MD', + 'PharmD', 'DVM', 'OD']) + ).count() + online_programs = Program.query.filter_by(is_online=True).count() + return render_template('admissions.html', + undergrad_programs=undergrad_programs, + grad_programs=grad_programs, + online_programs=online_programs) + + +@app.route('/about') +def about(): + stats = { + 'fulbright_rank': 1, + 'undergrad_majors': 200, + 'grad_programs': 278, + 'varsity_sports': 36, + 'faculty_count': 7000, + 'undergrad_count': 46820, + 'grad_count': 14000, + 'degree_programs': 500, + 'founded': 1870, + 'acres': 1665, + 'campuses': 6, + 'alumni': 600000, + 'extension_offices': 88, + 'buckeython_raised': 13, + 'research_expenditure': 1.3, + 'national_titles': 15, + } + return render_template('about.html', stats=stats) + + +@app.route('/search') +def search(): + q = request.args.get('q', '').strip() + results = {'programs': [], 'news': [], 'events': [], 'faculty': [], + 'research': [], 'athletics': []} + total = 0 + if q: + results['programs'] = ranked_search(Program.query.all(), ('name', 'description'), q) + results['news'] = ranked_search(NewsArticle.query.all(), ('title', 'summary', 'content', 'tags'), q) + results['events'] = ranked_search(Event.query.all(), ('title', 'description', 'location', 'organizer'), q) + results['faculty'] = ranked_search(Faculty.query.all(), ('name', 'research_interests', 'bio', 'title'), q) + results['research'] = ranked_search(ResearchCenter.query.all(), ('name', 'description', 'focus_areas', 'director'), q) + results['athletics'] = ranked_search(AthleticTeam.query.all(), ('name', 'sport', 'coach', 'home_venue'), q) + total = sum(len(values) for values in results.values()) + return render_template('search.html', q=q, results=results, total=total) + + +@app.route('/faculty') +def faculty(): + q = request.args.get('q', '').strip() + dept_slug = request.args.get('dept', '') + page = max(1, request.args.get('page', 1, type=int) or 1) + + query = Faculty.query + if q: + query = query.filter( + db.or_( + Faculty.name.ilike(f'%{q}%'), + Faculty.research_interests.ilike(f'%{q}%'), + Faculty.title.ilike(f'%{q}%'), + )) + if dept_slug: + dept = Department.query.filter_by(slug=dept_slug).first() + if dept: + query = query.filter(Faculty.department_id == dept.id) + + query = query.order_by(Faculty.name) + total = query.count() + faculty_list = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + all_depts = Department.query.order_by(Department.name).all() + return render_template('faculty.html', + faculty_list=faculty_list, + total=total, + page=page, + total_pages=total_pages, + all_depts=all_depts, + current_dept=dept_slug, + q=q) + + +@app.route('/faculty/') +def faculty_profile(slug): + member = Faculty.query.filter_by(slug=slug).first_or_404() + colleagues = [] + if member.department_id: + colleagues = Faculty.query.filter( + Faculty.department_id == member.department_id, + Faculty.id != member.id + ).limit(5).all() + return render_template('faculty_profile.html', member=member, colleagues=colleagues) + + +@app.route('/athletics') +def athletics(): + teams = AthleticTeam.query.order_by(AthleticTeam.sport, AthleticTeam.name).all() + men_teams = [t for t in teams if t.gender == 'Men'] + women_teams = [t for t in teams if t.gender == 'Women'] + coed_teams = [t for t in teams if t.gender == 'Co-ed'] + return render_template('athletics.html', + teams=teams, + men_teams=men_teams, + women_teams=women_teams, + coed_teams=coed_teams) + + +@app.route('/athletics/') +def athletics_team(slug): + team = AthleticTeam.query.filter_by(slug=slug).first_or_404() + related = AthleticTeam.query.filter( + AthleticTeam.gender == team.gender, + AthleticTeam.id != team.id + ).limit(4).all() + return render_template('athletics_team.html', team=team, related=related) + + +@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 user.check_password(form.password.data): + session.clear() + login_user(user) + next_page = safe_next(request.args.get('next'), url_for('index')) + flash('Welcome back, Buckeye!', 'success') + return redirect(next_page) + flash('Invalid email or password.', 'danger') + 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.lower().strip() + if User.query.filter((User.email == email) | (db.func.lower(User.username) == username)).first(): + flash('Unable to create an account with the supplied details.', 'danger') + else: + user = User( + email=email, + username=username, + full_name=form.full_name.data.strip(), + ) + user.set_password(form.password.data) + db.session.add(user) + try: + db.session.commit() + except IntegrityError: + db.session.rollback() + flash('Unable to create an account with the supplied details.', 'danger') + return render_template('register.html', form=form), 400 + session.clear() + login_user(user) + flash('Account created! Welcome to The Ohio State University.', 'success') + return redirect(url_for('index')) + return render_template('register.html', form=form) + + +@app.route('/logout', methods=['POST']) +@login_required +def logout(): + logout_user() + flash('You have been logged out.', 'info') + return redirect(url_for('index')) + + +@app.route('/account') +@login_required +def account(): + bookmarks = Bookmark.query.filter_by(user_id=current_user.id).order_by( + Bookmark.created_at.desc()).all() + bookmark_details = [] + for bm in bookmarks: + detail = {'bookmark': bm, 'item': None, 'title': '', 'url': '#'} + if bm.item_type == 'program': + item = db.session.get(Program, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('program_detail', slug=item.slug) + elif bm.item_type == 'news': + item = db.session.get(NewsArticle, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.title + detail['url'] = url_for('news_article', slug=item.slug) + elif bm.item_type == 'event': + item = db.session.get(Event, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.title + detail['url'] = url_for('event_detail', event_id=item.id) + elif bm.item_type == 'faculty': + item = db.session.get(Faculty, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('faculty_profile', slug=item.slug) + elif bm.item_type == 'research': + item = db.session.get(ResearchCenter, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('research_center', slug=item.slug) + elif bm.item_type == 'athletics': + item = db.session.get(AthleticTeam, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('athletics_team', slug=item.slug) + if detail['item'] is not None: + bookmark_details.append(detail) + return render_template('account.html', bookmark_details=bookmark_details) + + +@app.route('/bookmark/add', methods=['POST']) +@login_required +def bookmark_add(): + item_type = request.form.get('item_type', '').strip() + item_id = request.form.get('item_id', type=int) + model = BOOKMARK_TARGETS.get(item_type) + if model is None or not item_id: + abort(400) + if db.session.get(model, item_id) is None: + abort(404) + note = request.form.get('note', '').strip()[:500] + existing = Bookmark.query.filter_by( + user_id=current_user.id, item_type=item_type, item_id=item_id + ).first() + if not existing: + bookmark = Bookmark(user_id=current_user.id, item_type=item_type, + item_id=item_id, note=note) + db.session.add(bookmark) + try: + db.session.commit() + flash('Saved to bookmarks.', 'success') + except IntegrityError: + db.session.rollback() + flash('Already bookmarked.', 'info') + else: + flash('Already bookmarked.', 'info') + return redirect(safe_next(request.form.get('next'), url_for('account'))) + + +@app.route('/bookmark/remove', methods=['POST']) +@login_required +def bookmark_remove(): + bookmark_id = request.form.get('bookmark_id', type=int) + if bookmark_id: + bm = db.session.get(Bookmark, bookmark_id) + if bm and bm.user_id == current_user.id: + db.session.delete(bm) + db.session.commit() + flash('Bookmark removed.', 'info') + return redirect(safe_next(request.form.get('next'), url_for('account'))) + + +@app.route('/_health') +def health(): + try: + college_count = College.query.count() + program_count = Program.query.count() + return jsonify({ + 'ok': True, + 'site': 'osu', + 'colleges': college_count, + 'programs': program_count, + }) + except Exception as e: + return jsonify({'ok': False, 'message': str(e)}), 500 + + +@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 + + +# ─── Startup ────────────────────────────────────────────────────────────────── + +with app.app_context(): + db.create_all() + from seed_data import seed + seed() + +if __name__ == '__main__': + port = int(os.environ.get('PORT', SITE_PORT)) + app.run(host='0.0.0.0', port=port, debug=False) diff --git a/sites/osu/fetch_images.py b/sites/osu/fetch_images.py new file mode 100644 index 00000000..5aec2270 --- /dev/null +++ b/sites/osu/fetch_images.py @@ -0,0 +1,172 @@ +"""Fetch and normalize the OSU mirror's photographs from documented official source pages. + +Run with: uv run --with pillow python sites/osu/fetch_images.py +""" +from __future__ import annotations + +import hashlib +import io +import json +from pathlib import Path +from urllib.request import Request, urlopen + +from PIL import Image, ImageOps + +SITE_DIR = Path(__file__).resolve().parent +WEBP = SITE_DIR / "static" / "images" +WEBP.mkdir(parents=True, exist_ok=True) +SOURCES = [ + ( + "home-hero", + "A group of student entrepreneurs sit at a table and discuss their plans.", + "https://editing.intcomm.osu.edu/sites/default/files/2026-08/entrepreneurship_homepage1.jpg", + "https://www.osu.edu/", + ), + ( + "campus-life", + "A student playing guitar on the Oval during Ohio State’s involvement fair.", + "https://editing.intcomm.osu.edu/sites/default/files/inline-images/apply_26_675.jpg", + "https://www.osu.edu/", + ), + ( + "academics-undergraduate", + "A student wearing headphones paints on a canvas.", + "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-undergraduate-majors.jpg", + "https://www.osu.edu/academics", + ), + ( + "academics-graduate", + "Students work together in an academic setting.", + "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-graduate-degrees.jpg", + "https://www.osu.edu/academics", + ), + ( + "academics-online", + "A student wearing headphones studies on a laptop.", + "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-ohio-state-online.jpg", + "https://www.osu.edu/academics", + ), + ( + "research-hero", + "Ohio State research in action.", + "https://editing.intcomm.osu.edu/sites/default/files/inline-images/research-research-in-action.jpeg", + "https://www.osu.edu/research", + ), + ( + "research-mobility", + "A researcher works underneath an automobile.", + "https://editing.intcomm.osu.edu/sites/default/files/inline-images/Mobility_RI.jpg", + "https://www.osu.edu/research", + ), + ( + "research-microelectronics", + "A blue-gloved hand holds a microelectronic chip.", + "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/inline-images/microelectronics.jpg", + "https://www.osu.edu/research", + ), + ( + "about-education", + "A faculty member talks to students in front of a chalkboard.", + "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/about-education.jpg", + "https://www.osu.edu/about", + ), + ( + "about-health-care", + "A doctor smiles at a patient.", + "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/about-health-care.jpg", + "https://www.osu.edu/about", + ), + ( + "admissions-visit", + "Students visit the Ohio State campus.", + "https://undergrad.osu.edu/sites/UndergraduateAdmissions_22ac04/Images1/visit-campus-home.jpg", + "https://undergrad.osu.edu/", + ), + ( + "fisher-students", + "Students discuss a business case at Fisher College of Business.", + "https://s3.us-east-2.amazonaws.com/files.fisher.osu.edu/public/inline-images/Tab1_AcademicPrograms_0.jpg?VersionId=KnOdQG_NtxhHWMh2B1N8zOhDdc7wEYZx", + "https://fisher.osu.edu/", + ), + ( + "athletics-football", + "Ohio State football.", + "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F9%2F7%2F20250830_tdc_usa_065_large.jpg&width=1200&height=675&type=webp", + "https://ohiostatebuckeyes.com/", + ), + ( + "athletics-wrestling", + "Ohio State wrestler Nic Bouzakis competes at the Big Ten championships.", + "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F3%2F7%2FSZ9_5182_6dkzO.jpg&width=720&height=405&type=webp", + "https://ohiostatebuckeyes.com/sports/wrestling", + ), + ( + "athletics-basketball", + "Ohio State men’s basketball player John Mobley Jr. competes at the Big Ten tournament.", + "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F8%2F18%2FDJP04752_large.jpg&width=720&height=405&type=webp", + "https://ohiostatebuckeyes.com/sports/mens-basketball", + ), + ( + "athletics-fencing", + "Ohio State fencer Natalia Botello competes at the NCAA championships.", + "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F3%2F20%2F144A7346.JPG&width=720&height=405&type=webp", + "https://ohiostatebuckeyes.com/sports/fencing", + ), + ( + "james-cancer-hospital", + "The James Cancer Hospital and Solove Research Institute.", + "https://cancer.osu.edu/-/media/images/cancer/website/pages-and-carousels/about/locations/james-cancer-hospital.jpg", + "https://cancer.osu.edu/for-cancer-researchers", + ), + ( + "cancer-immunotherapy", + "A physician discusses immunotherapy.", + "https://cancer.osu.edu/-/media/images/cancer/website/pages-and-carousels/for-patients-and-caregivers/learn-about-cancers-and-treatments/specialized-treatment-clinics-and-centers/immunotherapy-management-clinic/dr-meara-discusses-immunotherapy.jpg", + "https://cancer.osu.edu/for-cancer-researchers", + ), + ( + "news-campus", + "The Ohio State University campus.", + "https://content.presspage.com/uploads/2170/800_ohiostatecampus-497663.jpg?10000", + "https://news.osu.edu/", + ), +] +manifest = [] +for name, alt, url, page in SOURCES: + req = Request( + url, + headers={"User-Agent": "Mozilla/5.0 (compatible; WebHarbor asset archival)"}, + ) + with urlopen(req, timeout=60) as r: + raw = r.read() + ctype = r.headers.get_content_type() + final = r.geturl() + if len(raw) < 5000: + raise RuntimeError((name, len(raw), ctype)) + with Image.open(io.BytesIO(raw)) as im: + im = ImageOps.exif_transpose(im).convert("RGB") + source_size = list(im.size) + im.thumbnail((1600, 1000), Image.Resampling.LANCZOS) + dest = WEBP / (name + ".webp") + im.save(dest, "WEBP", quality=84, method=6) + output_size = list(im.size) + manifest.append( + { + "file": name + ".webp", + "alt": alt, + "source_page": page, + "source_url": url, + "resolved_url": final, + "source_content_type": ctype, + "source_dimensions": source_size, + "output_dimensions": output_size, + "source_sha256": hashlib.sha256(raw).hexdigest(), + "output_sha256": hashlib.sha256(dest.read_bytes()).hexdigest(), + "output_bytes": dest.stat().st_size, + } + ) + print(name, ctype, source_size, "=>", output_size, dest.stat().st_size) +(SITE_DIR / "image_sources.json").write_text( + json.dumps({"images": manifest}, indent=2, ensure_ascii=False) + "\n" +) +print("TOTAL", sum(x["output_bytes"] for x in manifest)) diff --git a/sites/osu/image_sources.json b/sites/osu/image_sources.json new file mode 100644 index 00000000..83081069 --- /dev/null +++ b/sites/osu/image_sources.json @@ -0,0 +1,365 @@ +{ + "images": [ + { + "file": "home-hero.webp", + "alt": "A group of student entrepreneurs sit at a table and discuss their plans.", + "source_page": "https://www.osu.edu/", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/2026-08/entrepreneurship_homepage1.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/2026-08/entrepreneurship_homepage1.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1400, + 854 + ], + "output_dimensions": [ + 1400, + 854 + ], + "source_sha256": "b13eb65f21088f10e2ee98b4aed3c17f77a7415fcf29885b80ca74a3e90f84f3", + "output_sha256": "f9ceff1227c34d08ce766aea2dc50a149f15f86e7715dc2f569b1245039d31ff", + "output_bytes": 125284 + }, + { + "file": "campus-life.webp", + "alt": "A student playing guitar on the Oval during Ohio State’s involvement fair.", + "source_page": "https://www.osu.edu/", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/inline-images/apply_26_675.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/inline-images/apply_26_675.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 675 + ], + "output_dimensions": [ + 1200, + 675 + ], + "source_sha256": "a115748a38a982a21c28cbad1b521faa6a5df2422590fb8efbd9db9007ebbe60", + "output_sha256": "ea1d8bd9cb5ef053104e5c0f149f475e673764f206fa5ca954de17b55c969d2f", + "output_bytes": 161900 + }, + { + "file": "academics-undergraduate.webp", + "alt": "A student wearing headphones paints on a canvas.", + "source_page": "https://www.osu.edu/academics", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-undergraduate-majors.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-undergraduate-majors.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1076, + 605 + ], + "output_dimensions": [ + 1076, + 605 + ], + "source_sha256": "8b6fb5b40eb95bca2371db168547d370270e6c66609a1a3f4c2f23ce2a9a6b07", + "output_sha256": "abc4b950f8e268d1f1062de3a62d93f4328d83b06d18f85c8b19664c8c580e25", + "output_bytes": 97854 + }, + { + "file": "academics-graduate.webp", + "alt": "Students work together in an academic setting.", + "source_page": "https://www.osu.edu/academics", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-graduate-degrees.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-graduate-degrees.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1198, + 674 + ], + "output_dimensions": [ + 1198, + 674 + ], + "source_sha256": "68808e0c46bb7ec8f39ba2141fd3c58461e4b60c2a7f555cfc0118a53b5b3079", + "output_sha256": "5f00d76f9fb37675306522e162fe450ad5c1e90f58c6605e0e9f17fbe345f734", + "output_bytes": 81994 + }, + { + "file": "academics-online.webp", + "alt": "A student wearing headphones studies on a laptop.", + "source_page": "https://www.osu.edu/academics", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-ohio-state-online.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/academics-ohio-state-online.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1198, + 674 + ], + "output_dimensions": [ + 1198, + 674 + ], + "source_sha256": "d9778ae7a4bad60c9baae858d00de96ae46f1209ddee80b3137e115e8dd2d23b", + "output_sha256": "f8d88b3ec04da9946ed03f7b394c7ac3fafbf3b2ed36db7d685bee13386725d7", + "output_bytes": 55402 + }, + { + "file": "research-hero.webp", + "alt": "Ohio State research in action.", + "source_page": "https://www.osu.edu/research", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/inline-images/research-research-in-action.jpeg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/inline-images/research-research-in-action.jpeg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 800 + ], + "output_dimensions": [ + 1200, + 800 + ], + "source_sha256": "087b3df1da8036e8f300446c83f1a46c13d62efba4e2183178f1002056698700", + "output_sha256": "0776af58b36dd31d0c5a43f70e79a0b8cb9ba6404e3e9b5ab381c6fdbac70ad2", + "output_bytes": 46190 + }, + { + "file": "research-mobility.webp", + "alt": "A researcher works underneath an automobile.", + "source_page": "https://www.osu.edu/research", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/inline-images/Mobility_RI.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/inline-images/Mobility_RI.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 675 + ], + "output_dimensions": [ + 1200, + 675 + ], + "source_sha256": "a0b1019a0ef964b5e9c01adee57780ae92d2e68f97db8ecd6e0c4655dfcff997", + "output_sha256": "adf5913b1b51621f4a1a673619a9941239ea066d32a455ab00ce10aa6062a950", + "output_bytes": 84668 + }, + { + "file": "research-microelectronics.webp", + "alt": "A blue-gloved hand holds a microelectronic chip.", + "source_page": "https://www.osu.edu/research", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/inline-images/microelectronics.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/inline-images/microelectronics.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 675 + ], + "output_dimensions": [ + 1200, + 675 + ], + "source_sha256": "db9b1ddd6ad622965556b8ac268089d4d8c481bc007697e79fbeb8428dbc41d5", + "output_sha256": "d6248981af1acef4dedf3e13a18b540e793cc77990ea6886c4932b3cbfb5cae1", + "output_bytes": 82400 + }, + { + "file": "about-education.webp", + "alt": "A faculty member talks to students in front of a chalkboard.", + "source_page": "https://www.osu.edu/about", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/about-education.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/about-education.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 675 + ], + "output_dimensions": [ + 1200, + 675 + ], + "source_sha256": "a677e3264891fd77681094eed1b24eba35c9ef22f5fa3345e76f6da45ce1b60b", + "output_sha256": "db22e975d16e33600ee371b4ce1298a01853cbd55b79069c04944fe521788a2c", + "output_bytes": 53442 + }, + { + "file": "about-health-care.webp", + "alt": "A doctor smiles at a patient.", + "source_page": "https://www.osu.edu/about", + "source_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/about-health-care.jpg", + "resolved_url": "https://editing.intcomm.osu.edu/sites/default/files/styles/widescreen/public/media/image/2022/07/about-health-care.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 675 + ], + "output_dimensions": [ + 1200, + 675 + ], + "source_sha256": "0e851380d37ef36c87e33ec3facac2c64f97f2869d9f7a5df069c484cef062e3", + "output_sha256": "33915cd613e835bd5c1176807c9b2994c5b0e2cdff092a49b991a9be2343552e", + "output_bytes": 49000 + }, + { + "file": "admissions-visit.webp", + "alt": "Students visit the Ohio State campus.", + "source_page": "https://undergrad.osu.edu/", + "source_url": "https://undergrad.osu.edu/sites/UndergraduateAdmissions_22ac04/Images1/visit-campus-home.jpg", + "resolved_url": "https://undergrad.osu.edu/sites/UndergraduateAdmissions_22ac04/Images1/visit-campus-home.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 574, + 300 + ], + "output_dimensions": [ + 574, + 300 + ], + "source_sha256": "f10b2b38e90051ef962f96cab4db901f134271b3a9ef4dad141cf0fe8898c752", + "output_sha256": "aa6295ed5368156006e0b9b6e1c23796be0cabcc0c6528f9f31409a48b5848bb", + "output_bytes": 68174 + }, + { + "file": "fisher-students.webp", + "alt": "Students discuss a business case at Fisher College of Business.", + "source_page": "https://fisher.osu.edu/", + "source_url": "https://s3.us-east-2.amazonaws.com/files.fisher.osu.edu/public/inline-images/Tab1_AcademicPrograms_0.jpg?VersionId=KnOdQG_NtxhHWMh2B1N8zOhDdc7wEYZx", + "resolved_url": "https://s3.us-east-2.amazonaws.com/files.fisher.osu.edu/public/inline-images/Tab1_AcademicPrograms_0.jpg?VersionId=KnOdQG_NtxhHWMh2B1N8zOhDdc7wEYZx", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 345, + 230 + ], + "output_dimensions": [ + 345, + 230 + ], + "source_sha256": "9925b3aa97cd3b2666130986209144b890e74091d3b6f8a74de220f38f75f295", + "output_sha256": "b2198ffa231810c84c6b4b5a06553b6ce0e6706d68e0fbad84d43d6af671f299", + "output_bytes": 11362 + }, + { + "file": "athletics-football.webp", + "alt": "Ohio State football.", + "source_page": "https://ohiostatebuckeyes.com/", + "source_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F9%2F7%2F20250830_tdc_usa_065_large.jpg&width=1200&height=675&type=webp", + "resolved_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F9%2F7%2F20250830_tdc_usa_065_large.jpg&width=1200&height=675&type=webp", + "source_content_type": "image/webp", + "source_dimensions": [ + 1200, + 675 + ], + "output_dimensions": [ + 1200, + 675 + ], + "source_sha256": "e7a0d704aa83adcf3dce491c514adab060b623f741b1b1de507e629e74783392", + "output_sha256": "03f1bb4dec3ae10790a5c2b283e97e90083219ff0515ff628f4fbfbef583924f", + "output_bytes": 110532 + }, + { + "file": "athletics-wrestling.webp", + "alt": "Ohio State wrestler Nic Bouzakis competes at the Big Ten championships.", + "source_page": "https://ohiostatebuckeyes.com/sports/wrestling", + "source_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F3%2F7%2FSZ9_5182_6dkzO.jpg&width=720&height=405&type=webp", + "resolved_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F3%2F7%2FSZ9_5182_6dkzO.jpg&width=720&height=405&type=webp", + "source_content_type": "image/webp", + "source_dimensions": [ + 720, + 405 + ], + "output_dimensions": [ + 720, + 405 + ], + "source_sha256": "525360afe5d99ecbbf05e31cb417c74fe7537b6d65d9dd8cf91be246030ba7bb", + "output_sha256": "c15539639482319d9dda82bfb331d8ad4ec807ebb0468cff32396056b6f8af16", + "output_bytes": 32750 + }, + { + "file": "athletics-basketball.webp", + "alt": "Ohio State men’s basketball player John Mobley Jr. competes at the Big Ten tournament.", + "source_page": "https://ohiostatebuckeyes.com/sports/mens-basketball", + "source_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F8%2F18%2FDJP04752_large.jpg&width=720&height=405&type=webp", + "resolved_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F8%2F18%2FDJP04752_large.jpg&width=720&height=405&type=webp", + "source_content_type": "image/webp", + "source_dimensions": [ + 720, + 405 + ], + "output_dimensions": [ + 720, + 405 + ], + "source_sha256": "367102acc65eeec3031fc7b46e485ea77b00f17d7ac0568fc08c8750f1d303ac", + "output_sha256": "805b114fb66939b2e096099d297d37ec6d0e6d0b97c32e18dd34ab2c9de2923a", + "output_bytes": 30934 + }, + { + "file": "athletics-fencing.webp", + "alt": "Ohio State fencer Natalia Botello competes at the NCAA championships.", + "source_page": "https://ohiostatebuckeyes.com/sports/fencing", + "source_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F3%2F20%2F144A7346.JPG&width=720&height=405&type=webp", + "resolved_url": "https://images.sidearmdev.com/crop?url=https%3A%2F%2Fdxbhsrqyrr690.cloudfront.net%2Fsidearm.nextgen.sites%2Fohiostatebuckeyes.com%2Fimages%2F2026%2F3%2F20%2F144A7346.JPG&width=720&height=405&type=webp", + "source_content_type": "image/webp", + "source_dimensions": [ + 720, + 405 + ], + "output_dimensions": [ + 720, + 405 + ], + "source_sha256": "802a4d094eb1d715e3a6da324d80082fbe6978828ae829941cd20102c0e185ae", + "output_sha256": "86fa71fe903cfd99b2951485587397bfb0b7c4eb90612c47d83ea01e4bb0dee1", + "output_bytes": 22710 + }, + { + "file": "james-cancer-hospital.webp", + "alt": "The James Cancer Hospital and Solove Research Institute.", + "source_page": "https://cancer.osu.edu/for-cancer-researchers", + "source_url": "https://cancer.osu.edu/-/media/images/cancer/website/pages-and-carousels/about/locations/james-cancer-hospital.jpg", + "resolved_url": "https://cancer.osu.edu/-/media/images/cancer/website/pages-and-carousels/about/locations/james-cancer-hospital.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 700, + 525 + ], + "output_dimensions": [ + 700, + 525 + ], + "source_sha256": "958527d3763310c17f923d3dca83cd529c454b79903750492b60fe401f49de00", + "output_sha256": "2995744c6bb58c0cec2f7c0089cc755c253203a49c0488b765990cb377709e71", + "output_bytes": 71746 + }, + { + "file": "cancer-immunotherapy.webp", + "alt": "A physician discusses immunotherapy.", + "source_page": "https://cancer.osu.edu/for-cancer-researchers", + "source_url": "https://cancer.osu.edu/-/media/images/cancer/website/pages-and-carousels/for-patients-and-caregivers/learn-about-cancers-and-treatments/specialized-treatment-clinics-and-centers/immunotherapy-management-clinic/dr-meara-discusses-immunotherapy.jpg", + "resolved_url": "https://cancer.osu.edu/-/media/images/cancer/website/pages-and-carousels/for-patients-and-caregivers/learn-about-cancers-and-treatments/specialized-treatment-clinics-and-centers/immunotherapy-management-clinic/dr-meara-discusses-immunotherapy.jpg", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 1200, + 900 + ], + "output_dimensions": [ + 1200, + 900 + ], + "source_sha256": "a48518463057cbd6cf3fea15e7b35c9bd57f562ea35b002fde5369b1c3a6ba3c", + "output_sha256": "c1b1a2ae3237a78d9162ed61ff64b11351efc9c9a9f3cc0d943ea0fb26500c74", + "output_bytes": 50192 + }, + { + "file": "news-campus.webp", + "alt": "The Ohio State University campus.", + "source_page": "https://news.osu.edu/", + "source_url": "https://content.presspage.com/uploads/2170/800_ohiostatecampus-497663.jpg?10000", + "resolved_url": "https://content.presspage.com/uploads/2170/800_ohiostatecampus-497663.jpg?10000", + "source_content_type": "image/jpeg", + "source_dimensions": [ + 751, + 500 + ], + "output_dimensions": [ + 751, + 500 + ], + "source_sha256": "0450530446eaa3d47bcad631b177f116fa4b0313dcad35761b947a69b81809ff", + "output_sha256": "a6c84e3d869ea111a5abef6af36fa2f8ad7c49d201226ac0ee54f2e964f795cb", + "output_bytes": 118998 + } + ] +} diff --git a/sites/osu/requirements.txt b/sites/osu/requirements.txt new file mode 100644 index 00000000..acde45d9 --- /dev/null +++ b/sites/osu/requirements.txt @@ -0,0 +1,7 @@ +Flask +Flask-SQLAlchemy +Flask-Login +Flask-WTF +Flask-Bcrypt +email-validator +Pillow diff --git a/sites/osu/seed_data.py b/sites/osu/seed_data.py new file mode 100644 index 00000000..e2f60bcc --- /dev/null +++ b/sites/osu/seed_data.py @@ -0,0 +1,1067 @@ +#!/usr/bin/env python3 +"""Seed all Ohio State University mirror data. Idempotent.""" +from datetime import datetime, timedelta +import sys + +SEED_TIMESTAMP = datetime(2024, 10, 15, 12, 0, 0) +# Stable bcrypt hash for the public benchmark password "test1234". +BENCHMARK_PASSWORD_HASH = "$2b$12$nOIQFCC3iiGkjx3qLZY7e.z69INZu4oCSJbsu/HJvi6/F2zYQkoN." + + +def _app_module(): + module = sys.modules.get('app') + if module is not None: + return module + main = sys.modules.get('__main__') + if main is not None and hasattr(main, 'db') and hasattr(main, 'College'): + return main + import app as module + return module + + +def seed(): + """Seed all data once and reject partially initialized databases.""" + module = _app_module() + db = module.db + College = module.College + Department = module.Department + Program = module.Program + NewsArticle = module.NewsArticle + Event = module.Event + ResearchCenter = module.ResearchCenter + Faculty = module.Faculty + AthleticTeam = module.AthleticTeam + User = module.User + slugify = module.slugify + + counts = [model.query.count() for model in (College, Department, Program, NewsArticle, Event, ResearchCenter, Faculty, AthleticTeam, User)] + if all(counts): + return + if any(counts): + raise RuntimeError(f'OSU database is partially seeded: {counts}') + + # ── Helper ──────────────────────────────────────────────────────────────── + def _slug(text, extra=''): + base = slugify(text) + if extra: + base = base + '-' + slugify(extra) + return base + + # ───────────────────────────────────────────────────────────────────────── + # COLLEGES + # ───────────────────────────────────────────────────────────────────────── + college_data = [ + ('Arts and Sciences', 'Dean Patricia Bauer', 1870, 12000, 4000, + 'The College of Arts and Sciences is the intellectual and academic heart of Ohio State. ' + 'It offers over 80 majors and 200 graduate programs spanning the humanities, social sciences, ' + 'natural sciences, and mathematics. The college is home to more than 1,400 faculty and serves ' + 'roughly 16,000 undergraduate and graduate students.'), + ('Fisher College of Business', 'Dean Anil Makhija', 1916, 4500, 1800, + 'Fisher College of Business at Ohio State is consistently ranked among the nation\'s top ' + 'business schools. Fisher offers undergraduate, MBA, specialized master\'s, and doctoral ' + 'programs that prepare students for leadership in a globally connected marketplace.'), + ('Education and Human Ecology', 'Dean Carey Andrzejewski', 1895, 1800, 1200, + 'The College of Education and Human Ecology prepares leaders in education, human development, ' + 'family science, and nutrition. EHE faculty conduct research that improves lives at the ' + 'individual, family, community, and policy levels.'), + ('Engineering', 'Dean Ayanna Howard', 1870, 8000, 4500, + 'The College of Engineering at Ohio State is one of the largest and most comprehensive ' + 'engineering colleges in the United States. With 26 departments and research centers, ' + 'it drives economic growth and technological innovation across Ohio and beyond.'), + ('Food, Agricultural, and Environmental Sciences', 'Dean Cathann Kress', 1870, 3200, 1200, + 'CFAES advances knowledge at the intersection of agriculture, food, environment, and human ' + 'health. The college operates the Ohio Agricultural Research and Development Center and ' + 'the OSU Extension system across all 88 Ohio counties.'), + ('Moritz College of Law', 'Dean Wendy Smooth', 1891, 0, 650, + 'Moritz College of Law is one of the nation\'s leading law schools, offering the JD degree ' + 'and several specialized graduate programs. The college is known for its commitment to public ' + 'service, hands-on clinical education, and cutting-edge legal scholarship.'), + ('Medicine', 'Dean K. Craig Kent', 1914, 0, 900, + 'The Ohio State University College of Medicine is one of the largest medical schools in ' + 'the United States. Affiliated with the Wexner Medical Center and the James Cancer Hospital, ' + 'it is a national leader in medical education, research, and patient care.'), + ('Nursing', 'Dean Bernadette Melnyk', 1914, 600, 400, + 'The College of Nursing advances nursing science and prepares professional nurses for ' + 'leadership roles in health care. It offers BSN, MS, DNP, and PhD programs and is known ' + 'for its focus on wellness, evidence-based practice, and mental health.'), + ('Optometry', 'Dean Karla Zadnik', 1914, 200, 250, + 'The College of Optometry is recognized as one of the finest optometric colleges in the ' + 'world. It offers a four-year OD degree program and conducts pioneering research in ' + 'myopia, glaucoma, and vision rehabilitation.'), + ('Pharmacy', 'Dean Henry Mann', 1885, 300, 350, + 'The College of Pharmacy prepares pharmacists and pharmaceutical scientists to improve ' + 'medication therapy outcomes. Its PharmD program integrates cutting-edge research with ' + 'experiential learning at leading clinical sites across Ohio.'), + ('Public Health', 'Dean Amy Ferketich', 2012, 0, 600, + 'The College of Public Health educates future leaders committed to improving the health ' + 'of communities locally, nationally, and globally. Degree programs in epidemiology, health ' + 'behavior, environmental health, and health services management prepare graduates for ' + 'impact in government, industry, and research.'), + ('Social Work', 'Dean Tom Gregoire', 1914, 300, 500, + 'The College of Social Work is dedicated to advancing social and economic justice and ' + 'improving quality of life for people across the lifespan. Programs emphasize field ' + 'practice, evidence-based interventions, and policy advocacy.'), + ('Veterinary Medicine', 'Dean Rustin Moore', 1885, 400, 500, + 'The College of Veterinary Medicine is consistently ranked among the top vet schools in ' + 'the nation. Its teaching hospital serves tens of thousands of animal patients each year, ' + 'and its researchers pioneer breakthroughs in both animal and human health.'), + ('John Glenn College of Public Affairs', 'Dean Trevor Brown', 1999, 100, 350, + 'The John Glenn College of Public Affairs trains the next generation of public servants, ' + 'policy analysts, and nonprofit leaders. Named for the legendary Ohio astronaut and U.S. ' + 'Senator, the college emphasizes ethics, analytical rigor, and civic engagement.'), + ('Dentistry', 'Dean Kristin Williams', 1890, 0, 400, + 'The College of Dentistry provides comprehensive oral health care and trains outstanding ' + 'dental professionals. Its clinic serves tens of thousands of patients annually, and its ' + 'researchers advance knowledge in oral biology, dental materials, and community oral health.'), + ('Graduate School', 'Dean Sean Carson', 1878, 0, 14000, + 'The Graduate School oversees graduate education across all disciplines at Ohio State. ' + 'It supports master\'s, doctoral, and professional degree programs and fosters the ' + 'interdisciplinary research enterprise of the university.'), + ] + + colleges = {} + for name, dean, founded, ug, gr, desc in college_data: + c = College( + name=name, + slug=slugify(name), + dean=dean, + founded_year=founded, + undergrad_count=ug, + grad_count=gr, + description=desc, + ) + db.session.add(c) + colleges[name] = c + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # DEPARTMENTS + # ───────────────────────────────────────────────────────────────────────── + dept_data = [ + # Arts and Sciences + ('Department of Mathematics', 'Arts and Sciences', 'Dr. James Cogdell', + '(614) 292-4975', '100 Mathematics Building', + 'Mathematics at Ohio State encompasses research in algebra, analysis, geometry, ' + 'topology, logic, and applied mathematics. The department houses internationally ' + 'recognized faculty and produces PhD graduates who lead academia and industry.'), + ('Department of Physics', 'Arts and Sciences', 'Dr. Richard Furnstahl', + '(614) 292-5713', '174 W. 18th Avenue', + 'Physics research at Ohio State spans particle physics, condensed matter, biophysics, ' + 'and astrophysics. The department operates state-of-the-art experimental facilities ' + 'and has strong ties to national laboratories.'), + ('Department of Chemistry and Biochemistry', 'Arts and Sciences', 'Dr. Claudia Turro', + '(614) 292-2133', '100 W. 18th Avenue', + 'Chemistry and Biochemistry at Ohio State integrates teaching and research across ' + 'organic, inorganic, physical, analytical, and biological chemistry subfields.'), + ('Department of Computer Science and Engineering', 'Engineering', 'Dr. Srinivasan Parthasarathy', + '(614) 292-5813', '395 Dreese Lab', + 'CSE at Ohio State is at the forefront of computing research in machine learning, ' + 'security, systems, theory, and human-computer interaction. The department collaborates ' + 'with industry partners and national labs.'), + ('Department of Electrical and Computer Engineering', 'Engineering', 'Dr. Joel Johnson', + '(614) 292-3005', '205 Dreese Lab', + 'ECE research spans signal processing, electromagnetics, VLSI design, power systems, ' + 'and wireless communications. The department has strong industry partnerships and ' + 'state-of-the-art laboratories.'), + ('Department of Mechanical and Aerospace Engineering', 'Engineering', 'Dr. Marcelo Dapino', + '(614) 292-2288', '201 W. 19th Avenue', + 'MAE covers thermodynamics, fluid mechanics, solid mechanics, dynamics, robotics, ' + 'and aerospace systems. The department houses research centers including the Center ' + 'for Automotive Research.'), + ('Department of Economics', 'Arts and Sciences', 'Dr. Bruce Weinberg', + '(614) 292-0552', '1945 N. High Street', + 'The Department of Economics provides rigorous training in microeconomics, ' + 'macroeconomics, econometrics, and applied economics. Research spans labor, health, ' + 'development, public finance, and international economics.'), + ('Department of Psychology', 'Arts and Sciences', 'Dr. Steven Hecht', + '(614) 292-1739', '1835 Neil Avenue Mall', + 'Psychology at Ohio State integrates research and training in clinical, cognitive, ' + 'developmental, neuroscience, quantitative, and social psychology.'), + ('Department of History', 'Arts and Sciences', 'Dr. Stephanie Smith', + '(614) 292-2674', '104 Dulles Hall', + 'The History Department offers programs spanning American, European, African, Asian, ' + 'and world history. Faculty conduct research on topics from medieval Europe to ' + 'contemporary America.'), + ('Department of English', 'Arts and Sciences', 'Dr. Robyn Warhol', + '(614) 292-6065', '164 W. 17th Avenue', + 'English at Ohio State encompasses literary studies, creative writing, rhetoric and ' + 'composition, linguistics, and cultural studies. The department has a distinguished ' + 'tradition of interdisciplinary scholarship.'), + ('Department of Finance', 'Fisher College of Business', 'Dr. Kewei Hou', + '(614) 292-9470', '700 Fisher Hall', + 'Finance at Fisher prepares students for careers in investment banking, asset management, ' + 'corporate finance, and financial research. Research focuses on asset pricing, ' + 'corporate governance, and financial markets.'), + ('Department of Management and Human Resources', 'Fisher College of Business', 'Dr. David Greenberger', + '(614) 292-2311', '700 Fisher Hall', + 'The MHR department addresses critical topics in organizational behavior, human resource ' + 'management, leadership, and strategic management through rigorous research and ' + 'experiential learning.'), + ('Department of Biomedical Informatics', 'Medicine', 'Dr. Philip Payne', + '(614) 293-3600', '1800 Cannon Drive', + 'Biomedical Informatics bridges medicine, nursing, pharmacy, and computing to advance ' + 'health data science and clinical decision support. Faculty lead research in clinical ' + 'natural language processing, patient safety, and precision medicine.'), + ('Department of Epidemiology', 'Public Health', 'Dr. Amy Ferketich', + '(614) 292-0745', '1841 Neil Avenue', + 'Epidemiology faculty study the distribution and determinants of health and disease ' + 'in populations. Specialty areas include cancer, cardiovascular disease, infectious ' + 'disease, reproductive health, and social epidemiology.'), + ('Department of Environmental Engineering', 'Engineering', 'Dr. Linda Weavers', + '(614) 292-2006', '470 Hitchcock Hall', + 'Environmental Engineering addresses water, air, and soil quality challenges using ' + 'science and engineering principles. Research spans water treatment, remediation, ' + 'and sustainable infrastructure design.'), + ] + + departments = {} + for name, college_name, chair, phone, loc, desc in dept_data: + d = Department( + name=name, + slug=slugify(name), + college_id=colleges[college_name].id, + chair=chair, + phone=phone, + location=loc, + description=desc, + ) + db.session.add(d) + departments[name] = d + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # PROGRAMS + # ───────────────────────────────────────────────────────────────────────── + program_data = [ + # Undergrad BS/BA + ('Bachelor of Arts in Mathematics', 'BA', 'Arts and Sciences', 'Department of Mathematics', + 120, 4.0, 'December 1', False, False, + 'The BA in Mathematics provides students with a strong foundation in mathematical ' + 'reasoning and problem-solving. Students take courses in calculus, linear algebra, ' + 'abstract algebra, real analysis, and elective courses in their area of interest.', + 'Calculus sequence; Linear Algebra; Abstract Algebra; Real Analysis; 3 electives in pure or applied math.'), + ('Bachelor of Science in Computer Science and Engineering', 'BS', 'Engineering', 'Department of Computer Science and Engineering', + 130, 4.0, 'February 1', False, False, + 'The BS in CSE prepares students for careers in software engineering, systems design, ' + 'machine learning, and computer science research. The curriculum covers algorithms, ' + 'data structures, operating systems, computer architecture, and specialized electives.', + 'Calculus sequence; Physics; Programming fundamentals; Data Structures; Algorithms; OS; Compilers; Senior capstone.'), + ('Bachelor of Science in Mechanical Engineering', 'BS', 'Engineering', 'Department of Mechanical and Aerospace Engineering', + 132, 4.0, 'February 1', False, False, + 'The BS in Mechanical Engineering at Ohio State covers thermodynamics, fluid dynamics, ' + 'solid mechanics, dynamics, and machine design. Students gain hands-on experience in ' + 'state-of-the-art laboratories and design projects.', + 'Calculus; Physics; Engineering mechanics; Thermodynamics; Fluid mechanics; Materials science; Design capstone.'), + ('Bachelor of Science in Economics', 'BS', 'Arts and Sciences', 'Department of Economics', + 122, 4.0, 'February 1', False, False, + 'The BS in Economics offers rigorous training in economic theory, quantitative methods, ' + 'and applied economics. Graduates pursue careers in consulting, finance, government, ' + 'and economics research.', + 'Micro and macroeconomic theory; Econometrics; Math for economists; 5 field electives.'), + ('Bachelor of Arts in English', 'BA', 'Arts and Sciences', 'Department of English', + 120, 4.0, 'February 1', False, False, + 'The BA in English develops critical reading, research, and writing skills through the ' + 'study of literature, rhetoric, creative writing, and linguistics. Graduates succeed ' + 'in law, business, journalism, education, and many other fields.', + 'Foundations in literary study; Writing seminars; Literature survey courses; Upper-level seminars; Capstone.'), + ('Bachelor of Science in Physics', 'BS', 'Arts and Sciences', 'Department of Physics', + 130, 4.0, 'February 1', False, False, + 'The BS in Physics provides rigorous preparation in classical and modern physics, ' + 'mathematical methods, and experimental techniques. Students develop skills for ' + 'careers in research, engineering, education, and technology.', + 'Calculus-based physics sequence; Math methods; Quantum mechanics; E&M; Thermodynamics; Advanced lab; Capstone.'), + ('Bachelor of Science in Business Administration', 'BS', 'Fisher College of Business', 'Department of Finance', + 121, 4.0, 'December 1', False, False, + 'The BSBA at Fisher prepares students for leadership in diverse business environments. ' + 'Specializations include finance, marketing, management, supply chain, and information systems.', + 'Business core; Accounting; Statistics; Management; Finance; Marketing; Operations; Specialization courses; Capstone.'), + # Graduate + ('Master of Science in Computer Science', 'MS', 'Engineering', 'Department of Computer Science and Engineering', + 30, 2.0, 'December 15', False, False, + 'The MS in CSE at Ohio State offers depth in computer science theory and applications. ' + 'Students choose from thesis and non-thesis options and specialize in areas including ' + 'machine learning, systems, security, or algorithms.', + 'Core graduate CS courses; Thesis or project; 10 credit hours of electives.'), + ('Master of Science in Electrical and Computer Engineering', 'MS', 'Engineering', 'Department of Electrical and Computer Engineering', + 30, 2.0, 'January 15', False, True, + 'The MS in ECE prepares engineers for advanced work in signal processing, communications, ' + 'VLSI, power systems, and more. Both thesis and non-thesis options available.', + 'Graduate core courses; Electives; Thesis or final project.'), + ('Master of Business Administration', 'MBA', 'Fisher College of Business', 'Department of Management and Human Resources', + 60, 2.0, 'April 1', False, False, + 'The Fisher MBA develops business leaders through a rigorous curriculum combining ' + 'management fundamentals with real-world application. Specializations available in ' + 'finance, marketing, entrepreneurship, and operations.', + 'Business core; Leadership skills; Industry-specific electives; Business simulation; Capstone project.'), + ('Doctor of Philosophy in Mathematics', 'PhD', 'Arts and Sciences', 'Department of Mathematics', + 80, 5.0, 'January 1', False, True, + 'The PhD in Mathematics at Ohio State is a research-intensive program that prepares ' + 'students for academic and research careers. Students specialize in algebra, analysis, ' + 'geometry, topology, logic, or applied mathematics.', + 'Graduate coursework; Qualifying exams; Dissertation; Teaching duties.'), + ('Doctor of Philosophy in Physics', 'PhD', 'Arts and Sciences', 'Department of Physics', + 80, 5.5, 'December 1', False, True, + 'The PhD in Physics prepares students for careers in academic research, national ' + 'laboratories, and industry. Students conduct original research in particle physics, ' + 'condensed matter, biophysics, or astrophysics.', + 'Coursework; Qualifying exam; Research rotations; Dissertation.'), + ('Doctor of Philosophy in Computer Science', 'PhD', 'Engineering', 'Department of Computer Science and Engineering', + 80, 5.0, 'December 1', False, True, + 'The PhD in CS at Ohio State is a research-intensive program with national recognition ' + 'in machine learning, security, theoretical computer science, and human-computer ' + 'interaction. Students publish in top venues and collaborate with industry.', + 'Coursework; Candidacy exam; Research; Dissertation; Publications.'), + ('Juris Doctor', 'JD', 'Moritz College of Law', None, + 90, 3.0, 'April 1', False, False, + 'The JD at Moritz College of Law prepares students for practice in any legal field. ' + 'Known for experiential learning, the program offers more than 20 clinics, mock trial ' + 'competitions, and strong placement in law firms, government, and public service.', + 'Legal writing and research; Constitutional law; Contracts; Torts; Civil procedure; Professional responsibility; Electives; Clinical experience.'), + ('Doctor of Medicine', 'MD', 'Medicine', 'Department of Biomedical Informatics', + 0, 4.0, 'October 15', False, False, + 'The MD program at Ohio State College of Medicine offers exceptional training through ' + 'the unique Buckeye Transformative Education in Medicine curriculum. Students benefit ' + 'from early clinical experiences, research opportunities, and connections to the ' + 'nationally ranked Wexner Medical Center.', + 'Pre-clinical foundations; Clinical rotations; Research; Residency preparation.'), + ('Doctor of Pharmacy', 'PharmD', 'Pharmacy', None, + 0, 4.0, 'November 1', False, False, + 'The PharmD at Ohio State prepares pharmacists for clinical, research, and industry ' + 'careers. The curriculum integrates pharmaceutical sciences with patient-centered care ' + 'through experiential learning at outstanding clinical sites.', + 'Pharmaceutical sciences; Pharmacotherapy; Clinical rotations; Advanced practice experiences.'), + ('Doctor of Veterinary Medicine', 'DVM', 'Veterinary Medicine', None, + 0, 4.0, 'October 1', False, False, + 'The DVM program at Ohio State is consistently ranked among the top in the nation. ' + 'Students train at the Veterinary Medical Center, one of the nation\'s premier ' + 'veterinary teaching hospitals, and benefit from research opportunities across ' + 'all animal species.', + 'Biomedical sciences; Clinical sciences; Rotations; Research elective.'), + ('Doctor of Optometry', 'OD', 'Optometry', None, + 0, 4.0, 'October 1', False, False, + 'The OD program at Ohio State prepares optometrists for comprehensive patient care ' + 'in primary care, specialty, and research settings. Students gain extensive clinical ' + 'experience at the OSU Eye Center and affiliated sites.', + 'Optometric sciences; Clinical methods; Patient care rotations; Research.'), + ('Master of Public Health', 'MPH', 'Public Health', 'Department of Epidemiology', + 48, 2.0, 'February 1', True, False, + 'The MPH at Ohio State prepares public health professionals to assess, plan, and ' + 'implement programs that improve community health. Concentrations include epidemiology, ' + 'health behavior, environmental health, and health management.', + 'Core public health competencies; Concentration courses; Practicum; Capstone project.'), + ('Master of Science in Environmental Engineering', 'MS', 'Engineering', 'Department of Environmental Engineering', + 30, 2.0, 'January 15', False, False, + 'The MS in Environmental Engineering addresses water and wastewater treatment, ' + 'air quality, solid waste management, and environmental remediation. Both thesis ' + 'and non-thesis options are available.', + 'Graduate core; Electives; Thesis or project; Professional seminar.'), + ] + + for (name, deg, college_name, dept_name, units, dur, deadline, + is_online, gre, desc, reqs) in program_data: + college_obj = colleges.get(college_name) + dept_obj = departments.get(dept_name) if dept_name else None + slug = slugify(name) + '-' + deg.lower() + p = Program( + name=name, + slug=slug, + degree_type=deg, + college_id=college_obj.id if college_obj else None, + department_id=dept_obj.id if dept_obj else None, + units=units, + duration_years=dur, + application_deadline=deadline, + is_online=is_online, + gre_required=gre, + description=desc, + requirements=reqs, + ) + db.session.add(p) + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # RESEARCH CENTERS + # ───────────────────────────────────────────────────────────────────────── + research_data = [ + ('Translational Data Analytics Institute', 'TDAI', 'Dr. Beth Plale', + 'Arts and Sciences', 2016, + 'Data analytics, Machine learning, Health informatics, Social science', + 'https://tdai.osu.edu', + 'TDAI brings together faculty from across Ohio State to harness the power of data analytics ' + 'to address complex problems in society. The institute supports interdisciplinary research ' + 'in health, agriculture, smart cities, and social sciences.'), + ('Byrd Alzheimer\'s Center and Research Institute', 'Byrd', 'Dr. Douglas Scharre', + 'Medicine', 1987, + 'Alzheimer\'s disease, Dementia, Neuroimaging, Clinical trials', + '', + 'The Byrd Alzheimer\'s Center is dedicated to discovering causes and cures for Alzheimer\'s ' + 'disease and related dementias. Researchers conduct clinical trials and basic science studies ' + 'to advance diagnosis, prevention, and treatment.'), + ('Infectious Disease Institute', 'IDI', 'Dr. Michael Oglesbee', + 'Veterinary Medicine', 2008, + 'Infectious disease, Epidemiology, One Health, Vaccines', + 'https://idi.osu.edu', + 'IDI brings together virologists, bacteriologists, immunologists, and epidemiologists to ' + 'address infectious disease threats to humans, animals, and ecosystems through the One Health ' + 'approach. Research programs span HIV, influenza, SARS-CoV-2, and emerging pathogens.'), + ('Ohio Supercomputer Center', 'OSC', 'Dr. David Bickel', + 'Engineering', 1987, + 'High-performance computing, Scientific computing, Data storage, Visualization', + 'https://www.osc.edu', + 'The Ohio Supercomputer Center is a statewide resource supporting computational research ' + 'at Ohio State and institutions across Ohio. OSC provides high-performance computing, ' + 'data storage, and training to researchers in science, engineering, and the humanities.'), + ('Center for Automotive Research', 'CAR', 'Dr. Giorgio Rizzoni', + 'Engineering', 1991, + 'Electric vehicles, Autonomous driving, Energy storage, Powertrain systems', + 'https://car.osu.edu', + 'CAR partners with automotive industry leaders to advance electrification, autonomy, ' + 'connectivity, and mobility. Research programs address battery systems, vehicle dynamics, ' + 'driver behavior, and sustainable transportation systems.'), + ('Battelle Center for Science, Engineering and Public Policy', 'Battelle', 'Dr. Clay Johnston', + 'John Glenn College of Public Affairs', 2010, + 'Science policy, Technology policy, Energy policy, Climate policy', + '', + 'The Battelle Center examines how science and technology shape public policy choices. ' + 'Faculty and students analyze energy, environment, health, and security policy, ' + 'bridging the gap between scientific evidence and policy action.'), + ('James Cancer Hospital and Solove Research Institute', 'James', 'Dr. William Farrar', + 'Medicine', 1990, + 'Cancer research, Oncology, Clinical trials, Precision medicine', + 'https://cancer.osu.edu', + 'The James Cancer Hospital and Solove Research Institute is Ohio\'s only comprehensive ' + 'cancer center and ranks among the nation\'s top cancer programs. Researchers develop ' + 'novel immunotherapies, targeted treatments, and early detection methods.'), + ('Wexner Medical Center', 'WMC', 'Dr. Hal Paz', + 'Medicine', 1952, + 'Clinical medicine, Medical education, Translational research, Health systems', + 'https://wexnermedical.osu.edu', + 'The Ohio State Wexner Medical Center is a nationally recognized academic medical center ' + 'with hospitals, clinical programs, and research institutes advancing human health. ' + 'Researchers translate scientific discoveries into new diagnostics and therapies.'), + ('Drug Enforcement and Policy Center', 'DEPC', 'Dr. Douglas Berman', + 'Moritz College of Law', 2018, + 'Drug policy, Criminal justice, Marijuana policy, Opioid epidemic', + 'https://depc.osu.edu', + 'DEPC is the leading academic center on drug law and policy, examining enforcement, ' + 'regulation, and reform. Center experts provide evidence-based analysis on topics ' + 'including the opioid crisis, marijuana legalization, and sentencing reform.'), + ('Center for Clean Hydrogen', 'CCH', 'Dr. Yann Guezennec', + 'Engineering', 2022, + 'Hydrogen energy, Fuel cells, Green hydrogen, Energy storage', + '', + 'The Center for Clean Hydrogen advances science and engineering to enable a hydrogen ' + 'economy. Research addresses hydrogen production, storage, transportation, and fuel ' + 'cell technology for transportation and power generation applications.'), + ('Advanced Computing Center for the Arts and Design', 'ACCAD', 'Dr. Maria Palazzi', + 'Arts and Sciences', 1987, + 'Digital arts, Computer animation, Visualization, Motion capture', + 'https://accad.osu.edu', + 'ACCAD is an internationally recognized center for research and practice at the ' + 'intersection of art, design, and computing. Faculty and students develop new forms ' + 'of digital art, animation, interactive media, and scientific visualization.'), + ('Center for Cognitive and Brain Sciences', 'CCBS', 'Dr. Michael DeSchutter', + 'Arts and Sciences', 2007, + 'Neuroscience, Cognitive science, Decision making, Language', + '', + 'CCBS is an interdisciplinary research center addressing fundamental questions about ' + 'the brain and mind. Research programs span perception, attention, memory, decision ' + 'making, language, and social cognition using behavioral, neuroimaging, and computational methods.'), + ('Sustainability Institute', 'SI', 'Dr. Julie Newman', + 'Food, Agricultural, and Environmental Sciences', 2008, + 'Sustainability, Climate change, Campus operations, Environmental policy', + 'https://si.osu.edu', + 'The Sustainability Institute leads Ohio State\'s efforts to advance sustainability ' + 'in research, education, and campus operations. The institute supports interdisciplinary ' + 'research on energy, water, food systems, biodiversity, and climate resilience.'), + ('Chadwick Arboretum and Learning Gardens', 'Chadwick', 'Dr. Susan Pell', + 'Food, Agricultural, and Environmental Sciences', 1980, + 'Plant science, Horticulture, Biodiversity, Sustainable landscapes', + 'https://chadwickarboretum.osu.edu', + 'Chadwick Arboretum is a 60-acre living laboratory on the Columbus campus featuring ' + 'thousands of plant species. The arboretum supports research in plant science, ' + 'sustainable horticulture, and environmental education for the community.'), + ('Center for Biostatistics', 'CBS', 'Dr. Michael Pennell', + 'Public Health', 2001, + 'Biostatistics, Clinical trials, Epidemiology, Statistical genetics', + '', + 'The Center for Biostatistics provides statistical expertise and methodology development ' + 'to support clinical and public health research across Ohio State. Faculty collaborate ' + 'on clinical trials, cohort studies, and genomic data analysis.'), + ] + + rc_map = {} + for (name, short, director, college_name, founded, focus, url, desc) in research_data: + rc = ResearchCenter( + name=name, + slug=slugify(name), + director=director, + college_id=colleges.get(college_name, colleges['Arts and Sciences']).id, + founded_year=founded, + focus_areas=focus, + url=url, + description=desc, + ) + db.session.add(rc) + rc_map[name] = rc + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # FACULTY + # ───────────────────────────────────────────────────────────────────────── + faculty_data = [ + ('Dr. James Cogdell', 'Professor', 'Department of Mathematics', 'cogdell.1@osu.edu', 'MW 724', '(614) 292-4975', + 'Number theory, Automorphic forms, L-functions, Langlands program', + 'Professor Cogdell is a leading number theorist specializing in automorphic forms and the Langlands program. ' + 'He has received numerous awards and fellowships for his research contributions.', False), + ('Dr. Claudia Turro', 'Professor and Chair', 'Department of Chemistry and Biochemistry', 'turro.1@osu.edu', 'Evans 100D', '(614) 292-6567', + 'Inorganic photochemistry, Solar energy conversion, Anticancer agents, Ruthenium complexes', + 'Professor Turro leads research in inorganic photochemistry with applications in solar energy and cancer therapy. ' + 'Her group develops ruthenium-based complexes for photoactivated cancer treatment.', False), + ('Dr. Richard Furnstahl', 'Professor', 'Department of Physics', 'furnstahl.1@osu.edu', 'M2048 Physics Research Building', '(614) 292-4830', + 'Nuclear physics, Quantum chromodynamics, Effective field theory, Machine learning in physics', + 'Professor Furnstahl is a nuclear theorist who uses effective field theory and Bayesian methods ' + 'to study nuclear structure and reactions.', False), + ('Dr. Srinivasan Parthasarathy', 'Professor and Chair', 'Department of Computer Science and Engineering', 'parthasarathy.2@osu.edu', '591 Dreese Lab', '(614) 292-2568', + 'Data mining, Machine learning, Graph mining, Bioinformatics', + 'Professor Parthasarathy is an internationally recognized expert in data mining, machine learning, ' + 'and graph analytics. His group develops algorithms for large-scale data analysis with applications ' + 'in health care, social networks, and genomics.', False), + ('Dr. Marcelo Dapino', 'Professor and Honda R&D Americas Chair', 'Department of Mechanical and Aerospace Engineering', 'dapino.1@osu.edu', '201 W. 19th Avenue', '(614) 292-9138', + 'Smart materials, Vibration control, Automotive engineering, Magnetostrictive actuators', + 'Professor Dapino is a leading researcher in smart materials and structural acoustics, ' + 'with applications in automotive NVH and adaptive structures.', False), + ('Dr. Bruce Weinberg', 'Professor', 'Department of Economics', 'weinberg.27@osu.edu', '422 Arps Hall', '(614) 292-0553', + 'Labor economics, Innovation, Science of science, Health economics', + 'Professor Weinberg studies the economics of innovation, labor markets, and health. ' + 'His research on the relationship between age and scientific productivity has received ' + 'wide attention in academia and the popular press.', False), + ('Dr. Philip Payne', 'Professor and Chair', 'Department of Biomedical Informatics', 'payne.38@osu.edu', '1800 Cannon Drive', '(614) 293-3600', + 'Biomedical informatics, Clinical NLP, Precision medicine, Learning health systems', + 'Professor Payne leads the Department of Biomedical Informatics and is a pioneer in ' + 'data-driven approaches to clinical decision support and precision medicine.', False), + ('Dr. Amy Ferketich', 'Professor and Dean', 'Department of Epidemiology', 'ferketich.1@osu.edu', '250 Cunz Hall', '(614) 292-0745', + 'Tobacco control, Cancer epidemiology, Health disparities, Behavioral interventions', + 'Dean Ferketich is a nationally recognized tobacco control researcher who has led ' + 'population-based studies on smoking cessation, tobacco marketing, and health disparities.', False), + ('Dr. David Greenberger', 'Professor Emeritus', 'Department of Management and Human Resources', 'greenberger.1@osu.edu', '700 Fisher Hall', '(614) 292-0040', + 'Organizational behavior, Leadership, Work and family, Entrepreneurship', + 'Professor Greenberger is a pioneer in organizational behavior research and has made ' + 'foundational contributions to our understanding of leadership and organizational control.', True), + ('Dr. Kewei Hou', 'Professor', 'Department of Finance', 'hou.28@osu.edu', '750 Fisher Hall', '(614) 292-0552', + 'Asset pricing, Empirical finance, Factor models, Market anomalies', + 'Professor Hou is one of the leading empirical finance researchers of his generation, ' + 'known for the q-factor model of stock returns and research on market anomalies.', False), + ('Dr. Douglas Scharre', 'Professor and Director', 'Department of Epidemiology', 'scharre.1@osu.edu', '395 W. 12th Avenue', '(614) 293-4969', + 'Alzheimer\'s disease, Cognitive assessment, Dementia treatment, Brain aging', + 'Professor Scharre directs the Division of Cognitive Neurology and the Byrd Alzheimer\'s Center. ' + 'He developed the widely used Self-Administered Gerocognitive Exam (SAGE) for early ' + 'detection of cognitive impairment.', False), + ('Dr. Giorgio Rizzoni', 'Professor and Director', 'Department of Mechanical and Aerospace Engineering', 'rizzoni.1@osu.edu', '930 Kinnear Road', '(614) 292-0734', + 'Electric vehicles, Energy management, Hybrid powertrains, Control systems', + 'Professor Rizzoni is a global authority on electrified transportation and energy management ' + 'for hybrid and electric vehicles. He directs the Center for Automotive Research and has ' + 'led numerous multi-million dollar collaborative projects with automotive partners.', False), + ('Dr. Beth Plale', 'Professor and Executive Director', 'Department of Computer Science and Engineering', 'plale.1@osu.edu', '550 Dreese Lab', '(614) 292-1234', + 'Data science, Provenance, Research data management, Machine learning', + 'Professor Plale is Executive Director of the Translational Data Analytics Institute and ' + 'leads research in data science, provenance, and research data management.', False), + ('Dr. Linda Weavers', 'Professor and Chair', 'Department of Environmental Engineering', 'weavers.1@osu.edu', '470 Hitchcock Hall', '(614) 292-2006', + 'Water treatment, Sonochemistry, Environmental remediation, Emerging contaminants', + 'Professor Weavers is a leading expert in water treatment and sonochemical processes ' + 'for environmental remediation. Her research addresses treatment of emerging contaminants ' + 'including pharmaceuticals and PFAS.', False), + ('Dr. Douglas Berman', 'Professor and Director', 'Department of Economics', 'berman.43@osu.edu', '55 W. 12th Avenue', '(614) 292-5925', + 'Criminal law, Sentencing, Drug policy, Prison policy', + 'Professor Berman is the nation\'s leading academic expert on federal sentencing law and ' + 'drug policy reform. He founded and edits the widely read Sentencing Law and Policy blog ' + 'and directs the Drug Enforcement and Policy Center.', False), + ] + + for (name, title, dept_name, email, office, phone, interests, bio, emeritus) in faculty_data: + dept_obj = departments.get(dept_name) + m = Faculty( + name=name, + slug=slugify(name), + title=title, + department_id=dept_obj.id if dept_obj else None, + email=email, + office=office, + phone=phone, + research_interests=interests, + bio=bio, + is_emeritus=emeritus, + ) + db.session.add(m) + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # ATHLETIC TEAMS + # ───────────────────────────────────────────────────────────────────────── + team_data = [ + ('Ohio State Buckeyes Football', 'Football', 'Men', 'Ryan Day', 'Ohio Stadium (Horseshoe)', 8, '11-2'), + ('Ohio State Buckeyes Men\'s Basketball', 'Basketball', 'Men', 'Jake Diebler', 'Value City Arena', 0, '14-17'), + ('Ohio State Buckeyes Women\'s Basketball', 'Basketball', 'Women', 'Kevin McGuff', 'Value City Arena', 0, '21-12'), + ('Ohio State Buckeyes Baseball', 'Baseball', 'Men', 'Bill Mosiello', 'Bill Davis Stadium', 0, '35-22'), + ('Ohio State Buckeyes Softball', 'Softball', 'Women', 'Kelly Kovach Schoenly', 'Buckeye Field', 0, '26-25'), + ('Ohio State Buckeyes Men\'s Soccer', 'Soccer', 'Men', 'Brian Maisonneuve', 'Jesse Owens Memorial Stadium', 0, '7-10-3'), + ('Ohio State Buckeyes Women\'s Soccer', 'Soccer', 'Women', 'Lori Walker', 'Jesse Owens Memorial Stadium', 0, '13-7-3'), + ('Ohio State Buckeyes Men\'s Swimming & Diving', 'Swimming & Diving', 'Men', 'Bill Dorenkott', 'McCorkle Aquatic Pavilion', 12, '—'), + ('Ohio State Buckeyes Women\'s Swimming & Diving', 'Swimming & Diving', 'Women', 'Bill Dorenkott', 'McCorkle Aquatic Pavilion', 11, '—'), + ('Ohio State Buckeyes Men\'s Track & Field', 'Track & Field', 'Men', 'Ed Lomonaco', 'Jesse Owens Memorial Stadium', 0, '—'), + ('Ohio State Buckeyes Women\'s Track & Field', 'Track & Field', 'Women', 'Ed Lomonaco', 'Jesse Owens Memorial Stadium', 0, '—'), + ('Ohio State Buckeyes Volleyball', 'Volleyball', 'Women', 'Jen Flynn Oldenburg', 'Covelli Center', 0, '20-10'), + ('Ohio State Buckeyes Wrestling', 'Wrestling', 'Men', 'Tom Ryan', 'Covelli Center', 8, '17-4'), + ('Ohio State Buckeyes Men\'s Tennis', 'Tennis', 'Men', 'Ty Tucker', 'Ty Tucker Tennis Center', 0, '14-11'), + ('Ohio State Buckeyes Women\'s Tennis', 'Tennis', 'Women', 'Melissa Schaub', 'Ty Tucker Tennis Center', 0, '16-9'), + ('Ohio State Buckeyes Men\'s Golf', 'Golf', 'Men', 'Jay Moseley', 'OSU Golf Club', 0, '—'), + ('Ohio State Buckeyes Women\'s Golf', 'Golf', 'Women', 'Therese Hession', 'OSU Golf Club', 0, '—'), + ('Ohio State Buckeyes Men\'s Gymnastics', 'Gymnastics', 'Men', 'Miles Avery', 'Covelli Center', 0, '—'), + ('Ohio State Buckeyes Women\'s Gymnastics', 'Gymnastics', 'Women', 'Bob Fetter', 'Covelli Center', 0, '—'), + ('Ohio State Buckeyes Ice Hockey', 'Ice Hockey', 'Men', 'Steve Rohlik', 'Value City Arena', 0, '21-13-2'), + ('Ohio State Buckeyes Men\'s Lacrosse', 'Lacrosse', 'Men', 'Nick Myers', 'Ohio Stadium (turf)', 0, '14-5'), + ('Ohio State Buckeyes Women\'s Lacrosse', 'Lacrosse', 'Women', 'Nikki Hanigan', 'Buckeye Field (lacrosse)', 0, '8-10'), + ('Ohio State Buckeyes Field Hockey', 'Field Hockey', 'Women', 'Jarred Martin', 'Buckeye Field', 0, '12-8'), + ('Ohio State Buckeyes Rowing', 'Rowing', 'Women', 'Emanuele Catasta', 'Griggs Reservoir', 0, '—'), + ('Ohio State Buckeyes Fencing', 'Fencing', 'Men', 'George Shutt', 'RPAC', 2, '—'), + ('Ohio State Buckeyes Women\'s Fencing', 'Fencing', 'Women', 'George Shutt', 'RPAC', 0, '—'), + ] + + for (name, sport, gender, coach, venue, titles, record) in team_data: + t = AthleticTeam( + name=name, + slug=slugify(name), + sport=sport, + gender=gender, + conference='Big Ten', + coach=coach, + home_venue=venue, + national_titles=titles, + recent_record=record, + ) + db.session.add(t) + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # NEWS ARTICLES + # ───────────────────────────────────────────────────────────────────────── + now = datetime(2024, 10, 15) + articles = [ + ('Ohio State Researchers Develop Breakthrough Cancer Immunotherapy', 'Research', + 'Jody Sheridan', now - timedelta(days=2), True, + 'Ohio State, immunotherapy, cancer, James Cancer Hospital, research', + 'A team of Ohio State researchers has achieved a major breakthrough in cancer immunotherapy, ' + 'developing a novel approach that could improve outcomes for patients with hard-to-treat solid tumors.', + 'Ohio State researchers led by Dr. William Farrar at the James Cancer Hospital and Solove Research Institute ' + 'have developed a new CAR-T cell therapy that targets multiple tumor antigens simultaneously, ' + 'addressing one of the key limitations of current immunotherapy approaches.\n\n' + 'In preclinical studies, the multi-antigen approach demonstrated a 73 percent reduction in tumor burden ' + 'compared to conventional single-antigen CAR-T therapy. Researchers are now planning a Phase I clinical ' + 'trial expected to begin enrollment in early 2025.\n\n' + '"This represents a fundamental advance in how we design cellular therapies for solid tumors," ' + 'said Dr. Farrar. "By targeting multiple antigens, we make it much harder for cancer cells to ' + 'develop resistance through antigen escape."\n\n' + 'The research was published in Nature Medicine and was supported by grants from the National Cancer ' + 'Institute and the Ohio State University Comprehensive Cancer Center.'), + ('Ohio State Football Ranked in Top 5 Heading into Conference Play', 'Athletics', + 'Mike Cardamone', now - timedelta(days=3), True, + 'football, Buckeyes, Big Ten, ranking, Ryan Day', + 'The Ohio State Buckeyes football team has climbed into the top five of both major polls ' + 'as they enter the heart of Big Ten Conference play.', + 'The Ohio State Buckeyes have moved into the top five of the AP Poll and the Coaches Poll ' + 'after a dominant performance in their non-conference schedule. Coach Ryan Day\'s squad ' + 'has outscored opponents by an average of 34 points per game.\n\n' + '"We\'re playing complementary football right now," Day said at his Monday press conference. ' + '"The offense is efficient, the defense is flying around, and the special teams have been outstanding."\n\n' + 'Quarterback Will Howard has emerged as a Heisman Trophy candidate, completing 72 percent of ' + 'his passes for 1,847 yards and 18 touchdowns against just two interceptions.\n\n' + 'The Buckeyes host Michigan State this Saturday at Ohio Stadium, with kickoff set for noon ' + 'on Fox. The game is a sold-out affair, with over 105,000 fans expected to fill the Horseshoe.'), + ('Fisher College of Business Launches New Sustainability MBA Track', 'Campus Life', + 'OSU News Staff', now - timedelta(days=5), False, + 'Fisher, MBA, sustainability, business, environment', + 'Fisher College of Business has unveiled a new MBA specialization in Sustainable Business, ' + 'responding to growing demand from employers for business leaders with expertise in ESG.', + 'Fisher College of Business has announced the launch of a new Sustainable Business specialization ' + 'within its full-time MBA program, beginning in the 2025-2026 academic year.\n\n' + 'The specialization will allow MBA students to develop expertise in environmental, social, and ' + 'governance (ESG) strategy, sustainable supply chains, green finance, and corporate sustainability reporting.\n\n' + '"Employers are increasingly looking for business leaders who understand how sustainability creates ' + 'long-term value," said Dean Anil Makhija. "This specialization gives our students a competitive ' + 'advantage in a rapidly evolving landscape."\n\n' + 'The program includes case studies from Fortune 500 companies, a sustainability consulting practicum ' + 'with Ohio-based organizations, and access to Fisher\'s extensive alumni network in sustainable business.'), + ('Ohio Supercomputer Center Upgrades to Exascale-Class Computing', 'Research', + 'OSU News Staff', now - timedelta(days=7), False, + 'Ohio Supercomputer Center, HPC, computing, research infrastructure', + 'The Ohio Supercomputer Center has completed a major infrastructure upgrade, bringing ' + 'near-exascale computing capabilities to Ohio researchers.', + 'The Ohio Supercomputer Center has announced the successful deployment of Ascend, a new ' + 'high-performance computing cluster that dramatically expands computational capacity for ' + 'Ohio researchers.\n\n' + 'Ascend features 680 compute nodes with NVIDIA H100 GPUs, delivering over 50 petaflops of ' + 'AI-optimized computing power — a tenfold increase over the previous system. The system also ' + 'includes 10 petabytes of high-speed parallel storage.\n\n' + '"Ascend positions Ohio at the forefront of academic computing," said Director David Bickel. ' + '"Researchers can now tackle AI, climate modeling, drug discovery, and genomics problems ' + 'that were previously out of reach."'), + ('Ohio State Alumnus Appointed to NASA Administrator Role', 'Faculty', + 'OSU News Staff', now - timedelta(days=9), False, + 'alumni, NASA, space, science, engineering', + 'An Ohio State University alumnus with degrees in aerospace engineering and public policy ' + 'has been appointed to a senior leadership role at the National Aeronautics and Space Administration.', + 'Dr. James Crawford, who earned his BS in Aerospace Engineering from Ohio State in 1994 and his ' + 'MPH from the John Glenn College of Public Affairs in 2002, has been appointed as Deputy Associate ' + 'Administrator for Research at NASA.\n\n' + '"Ohio State gave me the foundation to dream big and the tools to make those dreams real," ' + 'Crawford said upon his appointment. "I carry the Buckeye spirit into everything I do."'), + ('College of Engineering Receives $50M Federal Grant for Hydrogen Research', 'Research', + 'College of Engineering Communications', now - timedelta(days=11), True, + 'engineering, hydrogen, energy, federal grant, NSF, research', + 'The Ohio State College of Engineering has been awarded a $50 million Department of Energy ' + 'grant to establish a national center for clean hydrogen research.', + 'The Ohio State College of Engineering has secured a $50 million grant from the Department of ' + 'Energy to establish the National Center for Clean Hydrogen Technologies, led by Professor ' + 'Yann Guezennec of the Center for Clean Hydrogen.\n\n' + 'The center will bring together researchers from engineering, chemistry, environmental engineering, ' + 'and public policy to accelerate the transition to a hydrogen economy.\n\n' + '"Hydrogen is one of the most promising pathways to deep decarbonization," said Dean Ayanna Howard. ' + '"This investment reflects Ohio State\'s leadership in clean energy research and positions us to ' + 'deliver real-world impact at scale."'), + ('Moritz College of Law Hosts National Symposium on Artificial Intelligence and Law', 'Campus Life', + 'Moritz Communications', now - timedelta(days=14), False, + 'law, AI, artificial intelligence, symposium, Moritz', + 'Moritz College of Law welcomed leading legal scholars, judges, and technologists for a ' + 'two-day national symposium on the intersection of artificial intelligence and legal practice.', + 'Moritz College of Law hosted more than 200 legal scholars, practicing attorneys, federal judges, ' + 'and technology experts at its inaugural AI and Law Symposium, exploring how machine learning ' + 'is transforming legal research, evidence, and decision-making.\n\n' + 'Keynote speakers included a federal circuit court judge who spoke about AI-assisted legal research, ' + 'and a leading AI ethicist who addressed bias and fairness in algorithmic decision systems.\n\n' + 'Symposium papers will be published in a special issue of the Ohio State Law Journal in spring 2025.'), + ('Ohio State Named Among Top 20 Public Universities by U.S. News', 'Campus Life', + 'OSU News Staff', now - timedelta(days=16), True, + 'ranking, U.S. News, public university, research', + 'The Ohio State University has been ranked among the top 20 public universities in the ' + 'United States in the latest U.S. News & World Report Best Colleges rankings.', + 'Ohio State maintained its position among the top 20 public universities in the country according ' + 'to the 2025 U.S. News & World Report Best Colleges rankings, continuing its streak of ' + 'consistent improvement over the past decade.\n\n' + 'Several individual programs also saw strong rankings: Computer Science moved into the top 15 ' + 'nationally; the Moritz College of Law climbed to #26; and the Fisher College of Business MBA ' + 'program is now ranked #28 in the nation.\n\n' + '"These rankings reflect the hard work of our faculty, staff, and students," said Provost Melissa Gilliam. ' + '"But more importantly, they reflect our commitment to providing a world-class education and ' + 'conducting research that matters."'), + ('TDAI Launches Interdisciplinary Health Data Science Training Program', 'Research', + 'TDAI Communications', now - timedelta(days=18), False, + 'TDAI, data science, health, training, interdisciplinary', + 'The Translational Data Analytics Institute has launched a new graduate training program ' + 'that brings together doctoral students from medicine, public health, statistics, and computer science.', + 'The Translational Data Analytics Institute has launched the Health Data Science Training Program, ' + 'a multi-year initiative funded by the National Institutes of Health to train the next generation ' + 'of health data scientists.\n\n' + 'The program provides 15 doctoral fellows per cohort with interdisciplinary coursework, mentored ' + 'research experiences, and professional development in communication, ethics, and entrepreneurship.\n\n' + '"Health data science requires expertise that no single discipline can provide," said Director Beth Plale. ' + '"This program creates researchers who can bridge clinical medicine, public health, and advanced computing."'), + ('Buckeyes Wrestling Team Ranked No. 1 in the Nation', 'Athletics', + 'OSU Athletics Communications', now - timedelta(days=20), False, + 'wrestling, ranking, national, Buckeyes, Tom Ryan', + 'The Ohio State wrestling team has opened the season as the No. 1 ranked team in the ' + 'nation, setting sights on a record-breaking national championship run.', + 'Ohio State wrestling began the 2024-25 season ranked No. 1 in the country by InterMat ' + 'and FloWrestling, led by a roster of 11 nationally ranked wrestlers including three ' + 'returning All-Americans.\n\n' + '"This team has the talent and the hunger to be special," said head coach Tom Ryan, who ' + 'has led the Buckeyes to 8 national championships. "But rankings don\'t win titles — ' + 'hard work and execution do."'), + ('Ohio State Sets Record for Research Expenditures at $1.3 Billion', 'Research', + 'Office of Research Communications', now - timedelta(days=22), True, + 'research, expenditure, record, funding, grants', + 'Ohio State has reached a historic milestone, surpassing $1.3 billion in annual research ' + 'expenditures for the first time in the university\'s 154-year history.', + 'The Ohio State University has reported a record $1.3 billion in research expenditures for ' + 'fiscal year 2024, according to data compiled by the Office of Research. This represents a ' + 'seven percent increase over the previous year and marks the university\'s highest-ever ' + 'research investment.\n\n' + 'Funding came from federal agencies including NIH, NSF, DOE, and DOD; state of Ohio; ' + 'industry partners; and private foundations.\n\n' + '"This milestone demonstrates the remarkable breadth and quality of research happening at ' + 'Ohio State," said President Ted Carter. "Our faculty are tackling the challenges that ' + 'matter most — cancer, climate, artificial intelligence, and so much more."'), + ('College of Nursing Launches Mental Health Initiative for Students', 'Health', + 'College of Nursing Communications', now - timedelta(days=25), False, + 'nursing, mental health, students, wellness, Bernadette Melnyk', + 'The College of Nursing, led by Dean Bernadette Melnyk, has launched a comprehensive ' + 'mental health and wellness initiative targeting Ohio State\'s student population.', + 'The Ohio State College of Nursing has launched "Buckeye Wellness," a university-wide ' + 'evidence-based program designed to improve mental health, well-being, and academic ' + 'outcomes for students.\n\n' + 'The program, developed by Dean Bernadette Melnyk and her team, provides students with ' + 'a seven-week cognitive behavioral skills-building intervention via a mobile application, ' + 'peer support groups, and faculty wellness champions.\n\n' + 'Early pilot data showed a 22 percent reduction in depression and anxiety symptoms among ' + 'participating students and a significant improvement in GPAs compared to a control group.'), + ('Ohio State Hosts International Climate Summit Ahead of COP30', 'Research', + 'Sustainability Institute', now - timedelta(days=28), False, + 'climate, sustainability, international, COP, environment', + 'The Ohio State Sustainability Institute welcomed climate researchers and policymakers ' + 'from 40 countries for a pre-COP30 research summit at the Columbus campus.', + 'Ohio State\'s Sustainability Institute hosted the Global Universities Climate Summit, ' + 'bringing together 300 climate scientists, economists, engineers, and policymakers from ' + '40 nations to share research and develop recommendations ahead of the UN Climate ' + 'Conference (COP30).\n\n' + '"Universities play a unique role in the climate challenge — we produce the knowledge ' + 'and the people who will deliver solutions," said Institute Director Dr. Julie Newman. ' + '"This summit strengthens the global network of researchers committed to a just and ' + 'sustainable future."\n\n' + 'Summit participants issued a joint statement calling for accelerated decarbonization ' + 'of electricity systems, food security investments, and equitable climate finance.'), + ('Department of Computer Science Launches AI Ethics Certificate', 'Student', + 'CSE Department Communications', now - timedelta(days=31), False, + 'CSE, AI, ethics, certificate, students', + 'The Department of Computer Science and Engineering has introduced a new undergraduate ' + 'certificate in Artificial Intelligence Ethics, open to students across all colleges.', + 'Ohio State\'s Department of Computer Science and Engineering has launched an undergraduate ' + 'Certificate in AI Ethics, a 15-credit interdisciplinary program available to students in ' + 'any major.\n\n' + 'The certificate curriculum draws on coursework from computer science, philosophy, law, ' + 'sociology, and public policy, preparing students to develop and deploy AI systems ' + 'responsibly.\n\n' + '"AI is being embedded into every sector of society, and we need graduates who understand ' + 'both the technical and ethical dimensions," said Department Chair Dr. Srinivasan Parthasarathy.'), + ('Ohio State Veterinary Medical Center Treats Record Number of Patients', 'Health', + 'College of Veterinary Medicine', now - timedelta(days=35), False, + 'veterinary, Vet Medical Center, patients, animals, care', + 'The Ohio State Veterinary Medical Center has seen a record 86,000 patient visits in ' + 'the past year, reinforcing its reputation as one of the nation\'s premier veterinary ' + 'teaching hospitals.', + 'The Ohio State University Veterinary Medical Center reported 86,231 patient visits ' + 'in fiscal year 2024, a 12 percent increase over the previous year and a new all-time record.\n\n' + 'The VMC offers specialty services across cardiology, dermatology, emergency medicine, ' + 'neurology, oncology, surgery, and more than 25 other disciplines for companion animals, ' + 'horses, and farm animals.\n\n' + '"Our growth reflects both the exceptional quality of our clinical teams and the trust ' + 'that pet owners across the region place in Ohio State," said Dean Rustin Moore.'), + ('Ohio State Announces New Partnership with Nationwide Children\'s Hospital', 'Health', + 'OSU Medical Center Communications', now - timedelta(days=40), False, + 'Nationwide Children\'s, partnership, pediatrics, medical center, research', + 'Ohio State and Nationwide Children\'s Hospital have formalized a landmark partnership ' + 'to advance pediatric research, education, and clinical care.', + 'The Ohio State University and Nationwide Children\'s Hospital have signed a comprehensive ' + 'partnership agreement that strengthens research, training, and patient care collaborations ' + 'between the two institutions.\n\n' + 'Under the agreement, Ohio State medical faculty will hold joint appointments at Nationwide ' + 'Children\'s, and residents in pediatrics will have expanded training opportunities at both ' + 'institutions.\n\n' + '"Together, Ohio State and Nationwide Children\'s are a powerhouse for children\'s health ' + 'in this region and nationally," said OSU President Ted Carter.'), + ('Buckeyes Swimming and Diving Teams Win Big Ten Championships', 'Athletics', + 'OSU Athletics Communications', now - timedelta(days=45), True, + 'swimming, diving, Big Ten, championship, Buckeyes', + 'Ohio State\'s men\'s and women\'s swimming and diving teams both captured Big Ten ' + 'Conference championships in dramatic fashion at the annual championships meet.', + 'Ohio State\'s swimming and diving programs claimed a sweep of the Big Ten championships, ' + 'with both the men\'s and women\'s teams winning conference titles for the second consecutive year.\n\n' + 'The men\'s team finished with 1,342.5 points, edging Michigan by 43 points, ' + 'while the women\'s team dominated with a 260-point margin of victory.\n\n' + 'Coach Bill Dorenkott\'s program has now won a combined 23 Big Ten championships.'), + ('Graduate School Expands Fellowship Opportunities for Doctoral Students', 'Student', + 'Graduate School Communications', now - timedelta(days=50), False, + 'graduate school, fellowship, doctoral, funding, PhD', + 'Ohio State\'s Graduate School has announced a $25 million expansion of fellowship ' + 'funding for doctoral students, aiming to increase diversity and reduce time to degree.', + 'Ohio State\'s Graduate School has announced the Buckeye Graduate Excellence Initiative, ' + 'a $25 million commitment over five years to expand fellowship support for doctoral students.\n\n' + 'The initiative will fund 150 additional university fellowships annually, with a particular ' + 'emphasis on recruiting students from underrepresented backgrounds and high-priority ' + 'research areas including AI, climate science, and biomedical engineering.\n\n' + '"Graduate students are the engine of our research enterprise," said Dean Sean Carson. ' + '"This investment ensures Ohio State can recruit and support the best and brightest from ' + 'across the country and the world."'), + ('Ohio State Extension Celebrates 100 Years of Service to Ohio Communities', 'Campus Life', + 'OSU Extension Communications', now - timedelta(days=55), False, + 'extension, Ohio, community, centennial, agriculture', + 'Ohio State University Extension is marking its centennial year with events across ' + 'all 88 Ohio counties, celebrating a century of research-based service to Ohioans.', + 'Ohio State University Extension is celebrating its 100th anniversary as an official ' + 'federal-state-local partnership, with events and programs planned across all 88 ' + 'Ohio counties throughout the academic year.\n\n' + 'Extension educators serve nearly every Ohio family through programs in agriculture, ' + 'family and consumer sciences, 4-H youth development, and community development.\n\n' + '"Extension connects the resources of Ohio State University directly to Ohioans wherever ' + 'they live," said College Dean Cathann Kress. "For 100 years, we have been a trusted ' + 'partner in making Ohio stronger."'), + ('Ohio State Partners with Intel on $200M Semiconductor Research Initiative', 'Research', + 'College of Engineering Communications', now - timedelta(days=60), True, + 'Intel, semiconductor, engineering, partnership, industry', + 'Ohio State University and Intel Corporation have announced a landmark $200 million ' + 'research partnership focused on next-generation semiconductor technology.', + 'Ohio State and Intel Corporation have announced a $200 million, 10-year research ' + 'partnership to advance semiconductor science and engineering.\n\n' + 'The collaboration will support research in chip design, manufacturing, materials science, ' + 'and workforce development, leveraging Intel\'s planned Ohio semiconductor fabrication ' + 'facilities in Licking County.\n\n' + '"This partnership is transformative for Ohio State and for the state of Ohio," said ' + 'Dean Ayanna Howard. "It connects world-class academic research directly to the largest ' + 'semiconductor investment in American history."\n\n' + 'The agreement includes joint faculty appointments, student internship pipelines, shared ' + 'laboratory facilities, and collaborative research grants.'), + ] + + for (title, cat, author, pub_date, featured, tags, summary, content) in articles: + a = NewsArticle( + title=title, + slug=slugify(title), + category=cat, + author=author, + published_date=pub_date, + featured=featured, + tags=tags, + summary=summary, + content=content, + view_count=0, + ) + db.session.add(a) + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # EVENTS + # ───────────────────────────────────────────────────────────────────────── + future = datetime(2024, 11, 1) + events = [ + ('Buckeyes vs. Michigan State Football', 'Sports', + future + timedelta(days=2), future + timedelta(days=2, hours=3, minutes=30), + 'Ohio Stadium', 'Ohio Stadium (The Horseshoe)', 'Columbus', 'OSU Athletics', False, 'Varies', + 'Come out to the Horseshoe as the Buckeyes take on the Michigan State Spartans in a key Big Ten ' + 'matchup. Enjoy pre-game festivities starting two hours before kickoff, including live music and ' + 'alumni tailgating. This is a sold-out game — tickets required.'), + ('Ohio State Research Forum: AI in Health Care', 'Lecture', + future + timedelta(days=5), future + timedelta(days=5, hours=2), + 'Biomedical Research Tower, Room 105', 'Biomedical Research Tower', 'Columbus', 'TDAI', True, 'Free', + 'Leading researchers from medicine, nursing, and computer science will present their work on ' + 'applying artificial intelligence to clinical decision support, medical imaging analysis, ' + 'and population health management. Lunch will be provided for registered attendees.'), + ('Annual Buckeye Career Fair — Engineering and Technology', 'Career', + future + timedelta(days=8), future + timedelta(days=8, hours=5), + 'Ohio Union', 'Ohio Union', 'Columbus', 'Engineering Career Services', True, 'Free', + 'Over 200 companies recruiting Ohio State students for internships, co-ops, and full-time ' + 'positions in engineering, technology, and related fields. Dress professionally and bring ' + 'multiple copies of your resume. Business casual or professional attire required.'), + ('Wexner Arts Center Performance: International Dance Festival', 'Arts', + future + timedelta(days=10), future + timedelta(days=10, hours=2), + 'Wexner Center for the Arts', 'Mershon Auditorium', 'Columbus', 'Wexner Center', False, '$15-35', + 'The Wexner Center\'s International Dance Festival presents world-renowned dance companies ' + 'in an evening of contemporary and classical works. This year features companies from ' + 'Brazil, South Korea, and France. Tickets available at the Wexner Center box office.'), + ('Ohio State Farmers Market', 'Social', + future + timedelta(days=12), future + timedelta(days=12, hours=3), + 'Tuttle Park Place', 'Tuttle Park', 'Columbus', 'OSU Sustainability Institute', False, 'Free', + 'The weekly Ohio State Farmers Market features fresh produce, artisan foods, and ' + 'handcrafted goods from local farmers and makers. Free to attend, cash and card accepted ' + 'at most vendors. Open to the campus community and Columbus neighbors.'), + ('Graduate Admissions Open House — College of Engineering', 'Lecture', + future + timedelta(days=15), future + timedelta(days=15, hours=3), + 'Dreese Laboratory Atrium', 'Dreese Lab', 'Columbus', 'Graduate School', True, 'Free', + 'Prospective graduate students in engineering are invited to learn about MS and PhD programs, ' + 'research opportunities, fellowship funding, and campus life at Ohio State. Department faculty ' + 'and current graduate students will be available for Q&A.'), + ('Infectious Disease Grand Rounds: Lessons from COVID-19', 'Lecture', + future + timedelta(days=18), future + timedelta(days=18, hours=1, minutes=30), + 'Meiling Hall Auditorium', 'Meiling Hall', 'Columbus', 'Infectious Disease Institute', False, 'Free', + 'Dr. Michael Oglesbee of the Infectious Disease Institute will lead a discussion on ' + 'surveillance, vaccine distribution, and pandemic preparedness lessons drawn from the ' + 'COVID-19 pandemic response. CME credit available for health care professionals.'), + ('Ohio State Women\'s Basketball Home Opener', 'Sports', + future + timedelta(days=20), future + timedelta(days=20, hours=2), + 'Value City Arena', 'Value City Arena', 'Columbus', 'OSU Athletics', False, 'Varies', + 'Cheer on the Buckeyes as the women\'s basketball team opens their home schedule at ' + 'Value City Arena. The team is led by experienced coach Kevin McGuff and returns several ' + 'key contributors from last year\'s NCAA Tournament team. Student tickets available.'), + ('Presidential Lecture Series: Dr. Ayanna Howard on Ethical AI', 'Lecture', + future + timedelta(days=23), future + timedelta(days=23, hours=1, minutes=30), + 'Research Commons, 18th Avenue Library', 'Libraries', 'Columbus', 'Office of Academic Affairs', False, 'Free', + 'Dean of Engineering Ayanna Howard presents a public lecture on the ethical dimensions ' + 'of artificial intelligence, drawing on her research in human-machine interaction and ' + 'algorithmic bias. This event is free and open to the campus community and the public.'), + ('Fisher College of Business Startup Competition Finals', 'Career', + future + timedelta(days=26), future + timedelta(days=26, hours=4), + 'Pfahl Hall Auditorium', 'Fisher Hall', 'Columbus', 'Fisher Entrepreneurship', True, 'Free', + 'Watch as student entrepreneurs pitch their companies to a panel of investors and business ' + 'leaders in the annual Fisher Startup Competition finals. Over $50,000 in prize money ' + 'and investment opportunities at stake. Open to the public — seats are limited.'), + ('Autumn Hike at Chadwick Arboretum', 'Social', + future + timedelta(days=29), future + timedelta(days=29, hours=2), + 'Chadwick Arboretum, 2001 Fyffe Court', 'Chadwick Arboretum', 'Columbus', 'Chadwick Arboretum', False, 'Free', + 'Enjoy a guided autumn hike through the Chadwick Arboretum with naturalist educators ' + 'highlighting seasonal plant changes, migratory birds, and sustainable landscaping practices. ' + 'Free and open to all — no registration required. Dogs welcome on leash.'), + ('Ohio Supercomputer Center Research Computing Workshop', 'Lecture', + future + timedelta(days=32), future + timedelta(days=32, hours=6), + 'Baker Systems Engineering Building', 'Baker Systems', 'Columbus', 'Ohio Supercomputer Center', True, 'Free', + 'Full-day workshop covering Ohio Supercomputer Center resources, job submission, ' + 'parallel programming, and GPU computing for research. Ideal for graduate students ' + 'and postdocs new to high-performance computing. Lunch provided for registered attendees.'), + ('CFAES Annual Farm Science Review', 'Social', + future + timedelta(days=40), future + timedelta(days=40, hours=8), + 'Molly Caren Agricultural Center', 'Molly Caren Agricultural Center', 'London', + 'CFAES Extension', False, '$15', + 'The Farm Science Review is one of the nation\'s largest agricultural shows, featuring ' + 'field demonstrations of the latest farm equipment, crop technologies, and agronomy ' + 'research. Held at the 1,000-acre Molly Caren Agricultural Center near London, Ohio.'), + ('Mental Health Awareness Week: Buckeye Wellness Fair', 'Health', + future + timedelta(days=7), future + timedelta(days=7, hours=4), + 'Ohio Union Great Hall', 'Ohio Union', 'Columbus', 'Student Life Counseling and Consultation Service', False, 'Free', + 'Ohio State\'s annual Mental Health Awareness Week features a wellness fair with ' + 'information about campus counseling resources, peer support programs, mindfulness ' + 'workshops, and interactive activities promoting mental well-being. All students welcome.'), + ('Law Review Symposium: Technology, Privacy, and the Law', 'Lecture', + future + timedelta(days=35), future + timedelta(days=35, hours=8), + 'Drinko Hall Auditorium', 'Moritz College of Law', 'Columbus', 'Ohio State Law Journal', True, 'Free', + 'The Ohio State Law Journal\'s annual symposium brings together leading scholars and ' + 'practitioners to examine legal frameworks around data privacy, surveillance, ' + 'algorithmic decision-making, and digital rights. CLE credit available.'), + ('Virtual Info Session: Online MPH Program', 'Virtual', + future + timedelta(days=14), future + timedelta(days=14, hours=1), + 'Zoom (link provided upon registration)', 'Online', 'Columbus', + 'College of Public Health', True, 'Free', + 'Learn about Ohio State\'s online Master of Public Health program, including curriculum, ' + 'admissions requirements, financial aid, and student experience. Faculty and current ' + 'students will be available to answer questions. Registration required to receive Zoom link.'), + ] + + for (title, cat, start, end, location, building, campus, organizer, reg_req, cost, desc) in events: + e = Event( + title=title, + description=desc, + start_datetime=start, + end_datetime=end, + location=location, + building=building, + campus=campus, + category=cat, + organizer=organizer, + registration_required=reg_req, + cost=cost, + ) + db.session.add(e) + db.session.flush() + + # ───────────────────────────────────────────────────────────────────────── + # BENCHMARK USERS + # ───────────────────────────────────────────────────────────────────────── + if not User.query.filter_by(email='alice@osu.edu').first(): + for username, name, email, role in [ + ('alice', 'Alice Anderson', 'alice@osu.edu', 'student'), + ('bob', 'Bob Baker', 'bob@osu.edu', 'student'), + ('carol', 'Carol Chen', 'carol@osu.edu', 'faculty'), + ('dave', 'Dave Davis', 'dave@osu.edu', 'staff'), + ]: + u = User(username=username, email=email, full_name=name, role=role, + password_hash=BENCHMARK_PASSWORD_HASH, + created_at=SEED_TIMESTAMP + timedelta(seconds=len(username))) + db.session.add(u) + db.session.flush() + + db.session.commit() diff --git a/sites/osu/static/css/.gitkeep b/sites/osu/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/osu/static/js/.gitkeep b/sites/osu/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/osu/tasks.jsonl b/sites/osu/tasks.jsonl new file mode 100644 index 00000000..d1f8008c --- /dev/null +++ b/sites/osu/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name":"Ohio State University","id":"Ohio State University--0","ques":"Open Academics and report the dean listed for Fisher College of Business.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_0.py","judge_rubric":"FACT CHECKPOINTS: The same-origin Academics page must be visited. The answer must affirmatively identify Anil Makhija as Fisher College of Business dean. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--1","ques":"Open About and report the number displayed for Varsity Sports.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_1.py","judge_rubric":"FACT CHECKPOINTS: The same-origin About page must be visited. The answer must bind 36 to Varsity Sports. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--2","ques":"Open Athletics, then visit both the football and wrestling team pages. Which conference do both pages list?","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_2.py","judge_rubric":"FACT CHECKPOINTS: Athletics must precede visible clicks to both exact football and wrestling details. The answer must affirmatively report Big Ten for both. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--3","ques":"From Athletics, open the Ohio State Buckeyes Football page and report the head coach and recent record.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_3.py","judge_rubric":"FACT CHECKPOINTS: Athletics must precede a click to the exact football detail. The answer must bind Ryan Day to head coach and 11-2 to recent record. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--4","ques":"Search Ohio State for 'research expenditures', open the article about the $1.3 billion record, and report the expenditure amount and publication date.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_4.py","judge_rubric":"FACT CHECKPOINTS: A same-origin search for research expenditures must precede a click to the exact record-expenditure article. The answer must report $1.3 billion and September 23, 2024. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--5","ques":"Open About and report both the university's founding year and its original institution name.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_5.py","judge_rubric":"FACT CHECKPOINTS: About must be visited. The answer must bind 1870 and Ohio Agricultural and Mechanical College to the requested facts. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--6","ques":"Open Research, then the Translational Data Analytics Institute page. Report its director and all four focus areas shown.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_6.py","judge_rubric":"FACT CHECKPOINTS: Research must precede a click to the exact TDAI detail. The answer must report Beth Plale and Data analytics, Machine learning, Health informatics, and Social science. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--7","ques":"Open About and report the undergraduate and graduate enrollment counts and the exact difference between them.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_7.py","judge_rubric":"FACT CHECKPOINTS: About must be visited. The answer must bind 46,820 undergraduates, 14,000 graduate students, and difference 32,820. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--8","ques":"Open Academics and compare Engineering with Fisher College of Business. Report both undergraduate counts, which college has more, and the exact difference.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_8.py","judge_rubric":"FACT CHECKPOINTS: Academics must be visited. The answer must bind 8,000 to Engineering, 4,500 to Fisher, identify Engineering as higher, and report difference 3,500. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--9","ques":"Use the Programs college filter for Engineering. Report all distinct degree types shown in those filtered results and how many distinct types there are.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_9.py","judge_rubric":"FACT CHECKPOINTS: One /programs URL must contain college=engineering. The answer must report exactly the distinct types BS, MS, and PhD and count 3. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--10","ques":"From Athletics, open the Ohio State Buckeyes Wrestling page and report its head coach and home venue.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_10.py","judge_rubric":"FACT CHECKPOINTS: Athletics must precede a click to wrestling detail. The answer must bind Tom Ryan to head coach and Covelli Center to home venue. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--11","ques":"Open Research, then the Ohio Supercomputer Center page. Report its director and founding year.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_11.py","judge_rubric":"FACT CHECKPOINTS: Research must precede a click to Ohio Supercomputer Center. The answer must report David Bickel and 1987. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--12","ques":"Search Programs for 'Juris Doctor', open the JD detail page, and report its degree type, credit count, and duration.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_12.py","judge_rubric":"FACT CHECKPOINTS: A /programs?q=Juris Doctor visit must precede a click to the exact JD detail. The answer must bind JD, 90 credits, and 3 years. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--13","ques":"Open Departments, then the Department of Mathematics page. Report the department chair and location.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_13.py","judge_rubric":"FACT CHECKPOINTS: Departments must precede a click to the exact Mathematics detail. The answer must report James Cogdell and 100 Mathematics Building. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--14","ques":"From Athletics, open both the football and men's basketball team pages. Report the home venue for each team.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_14.py","judge_rubric":"FACT CHECKPOINTS: Athletics must precede clicks to both football and men's basketball details. The answer must bind Ohio Stadium to football and Value City Arena to men's basketball. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--15","ques":"From Athletics, open the wrestling and fencing team pages. Report both national championship counts, identify which has more, and give the exact difference.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_15.py","judge_rubric":"FACT CHECKPOINTS: Athletics must precede clicks to wrestling and fencing details. The answer must bind 8 titles to wrestling and 2 to fencing, identify wrestling as higher, and report difference 6. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--16","ques":"Use the Programs degree filter for MBA, open the Master of Business Administration page, and report its application deadline, credit count, and whether the GRE is required.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_16.py","judge_rubric":"FACT CHECKPOINTS: One /programs URL must contain degree=MBA before the exact MBA detail is clicked. The answer must report April 1, 60 credits, and GRE Not Required. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--17","ques":"Search Ohio State for 'cancer research', open the cancer immunotherapy breakthrough news article, and report its exact title and author.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_17.py","judge_rubric":"FACT CHECKPOINTS: A same-origin search for cancer research must precede a click to the exact cancer immunotherapy article. The answer must report the exact title and Jody Sheridan. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--18","ques":"Open Research, then the James Cancer Hospital and Solove Research Institute page. Report its director and all four focus areas shown.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_18.py","judge_rubric":"FACT CHECKPOINTS: Research must precede a click to the exact James detail. The answer must report William Farrar and Cancer research, Oncology, Clinical trials, and Precision medicine. The complete database must remain unchanged."} +{"web_name":"Ohio State University","id":"Ohio State University--19","ques":"Open Research, then the Center for Clean Hydrogen page. Report its director, founding year, and all four focus areas shown.","web":"http://localhost:40020/","upstream_url":"https://www.osu.edu/","verifier_path":"sites/osu/verify/verify_19.py","judge_rubric":"FACT CHECKPOINTS: Research must precede a click to the exact Clean Hydrogen detail. The answer must report Yann Guezennec, 2022, and Hydrogen energy, Fuel cells, Green hydrogen, and Energy storage. The complete database must remain unchanged."} diff --git a/sites/osu/templates/404.html b/sites/osu/templates/404.html new file mode 100644 index 00000000..b47a84a4 --- /dev/null +++ b/sites/osu/templates/404.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block title %}Page Not Found — Ohio State{% endblock %} +{% block content %} +
+
404
+

Page Not Found

+

+ The page you're looking for doesn't exist or has been moved. +

+ +
+{% endblock %} diff --git a/sites/osu/templates/500.html b/sites/osu/templates/500.html new file mode 100644 index 00000000..a5ada03a --- /dev/null +++ b/sites/osu/templates/500.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}Server Error — Ohio State{% endblock %} +{% block content %} +
+
500
+

Internal Server Error

+

+ Something went wrong on our end. Please try again later. +

+ Return to Homepage +
+{% endblock %} diff --git a/sites/osu/templates/about.html b/sites/osu/templates/about.html new file mode 100644 index 00000000..aff3c553 --- /dev/null +++ b/sites/osu/templates/about.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} +{% block title %}About Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('about-education') %} +
{{ banner.alt }}
+ + +
+
+
{{ stats.founded }}
+
Year Founded
+
+
+
{{ (stats.undergrad_count + stats.grad_count) | int }}
+
Total Enrollment
+
+
+
600K+
+
Living Alumni
+
+
+
{{ stats.acres }}
+
Acres (Columbus Campus)
+
+
+ + +
+

Our History

+
+
+

The Ohio State University was established in 1870 as the Ohio Agricultural and Mechanical College under the Morrill Land-Grant Colleges Act. Since then, it has grown to become one of the largest universities in the United States.

+

The university's flagship Columbus campus spans 1,665 acres in the heart of Ohio's capital city, with additional regional campuses in Lima, Mansfield, Marion, Newark, and Wooster.

+

As a leading public research university, Ohio State has been at the forefront of discovery, producing Nobel laureates, Rhodes Scholars, Fulbright Scholars, and countless innovators who have shaped American society and the world.

+
+
+

Timeline Highlights

+
+ {% for year, event in [ + (1870, 'Founded as Ohio Agricultural and Mechanical College'), + (1878, 'Renamed The Ohio State University'), + (1916, 'Ohio Stadium built — The Horseshoe'), + (1930, 'Named to Association of American Universities'), + (1960, 'Jesse Owens wins 4 gold medals at Berlin Olympics (OSU alum)'), + (1967, 'Wexner Medical Center opens'), + (1980, 'National Supercomputer Center established'), + (1995, 'Online education programs launched'), + (2002, 'Football National Championship'), + (2014, 'Football National Championship'), + (2024, 'Research expenditure exceeds $1.3 billion') + ] %} +
+
{{ year }}
+
{{ event }}
+
+ {% endfor %} +
+
+
+
+ + +
+

Mission & Values

+
+
+

Mission

+

The Ohio State University advances the well-being of the people of Ohio and the global community through the creation and dissemination of knowledge.

+
+
+

Vision

+

To be the model 21st-century public research university — an institution that is as innovative, resourceful and diverse as the challenges we exist to solve.

+
+
+

Core Values

+
    +
  • ◆ Excellence in education
  • +
  • ◆ Research and discovery
  • +
  • ◆ Outreach and engagement
  • +
  • ◆ Diversity and inclusion
  • +
  • ◆ Integrity and ethics
  • +
+
+
+
+ + +
+

University Leadership

+
+ {% for title, name, desc in [ + ('President', 'Dr. Ted Carter', 'The president provides executive leadership and is the chief administrative officer of the university.'), + ('Provost', 'Dr. Melissa Gilliam', 'The provost is the chief academic officer, overseeing educational programs and research.'), + ('Board of Trustees', 'John W. Zeiger, Chair', 'The Board of Trustees governs the university and ensures it fulfills its mission.') + ] %} +
+
+
{{ title }}
+
{{ name }}
+

{{ desc }}

+
+
+ {% endfor %} +
+
+ + +
+

Ohio State by the Numbers

+
+ {% for num, label in [ + (stats.undergrad_count | int, 'Undergraduates'), + (stats.grad_count | int, 'Graduate Students'), + (stats.faculty_count | int, 'Faculty Members'), + (stats.degree_programs, 'Degree Programs'), + (stats.national_titles, 'National Athletic Titles'), + (stats.varsity_sports, 'Varsity Sports'), + (stats.extension_offices, 'Extension Offices'), + (stats.campuses, 'Campuses') + ] %} +
+
{{ num }}
+
{{ label }}
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/sites/osu/templates/academics.html b/sites/osu/templates/academics.html new file mode 100644 index 00000000..f2e045e7 --- /dev/null +++ b/sites/osu/templates/academics.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} +{% block title %}Academics — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('academics-undergraduate') %} +
{{ banner.alt }}
+ +
+ Ohio State is a top-20 public research university with world-class programs across arts, sciences, engineering, medicine, and more. +
+ +

Colleges & Schools

+
+ {% for college in colleges %} + {% set college_photo = college_image(college) %} +
+ {{ college_photo.alt }} +
+

{{ college.name }}

+

{{ college.description[:140] }}{% if college.description|length > 140 %}...{% endif %}

+
+ {% if college.dean %}
Dean: {{ college.dean }}
{% endif %} +
{{ college.undergrad_count | default(0) }} undergrad • {{ college.grad_count | default(0) }} graduate
+
Founded {{ college.founded_year }}
+
+ +
+
+ {% endfor %} +
+ +
+

Academic Resources

+
+
+
{{ total_programs }}
+
Degree Programs
+
+
+
{{ total_depts }}
+
Departments
+
+
+
{{ colleges|length }}
+
Colleges
+
+
+
6
+
Campuses
+
+
+
+{% endblock %} diff --git a/sites/osu/templates/account.html b/sites/osu/templates/account.html new file mode 100644 index 00000000..9f84bcd7 --- /dev/null +++ b/sites/osu/templates/account.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} +{% block title %}My Account — Ohio State{% endblock %} +{% block content %} +
+

My Account

+

Welcome, {{ current_user.full_name or current_user.username }}!

+ + +
+
+
+
+ {{ (current_user.full_name or current_user.username)[0].upper() }} +
+
+
{{ current_user.full_name or current_user.username }}
+
{{ current_user.email }}
+
Member since {{ current_user.created_at.strftime('%B %Y') }}
+ {% if current_user.bio %}

{{ current_user.bio }}

{% endif %} +
+
+ {{ current_user.role | title }} +
+
+
+
+ + +

My Saved Items ({{ bookmark_details|length }})

+ {% if bookmark_details %} + {% for detail in bookmark_details %} +
+
+
+
+ {{ detail.bookmark.item_type }} + + {% if detail.bookmark.note %} +
Note: {{ detail.bookmark.note }}
+ {% endif %} +
Saved {{ detail.bookmark.created_at.strftime('%B %d, %Y') }}
+
+
+ + + + +
+
+
+
+ {% endfor %} + {% else %} +
+
+

No saved items yet

+

Bookmark programs, news, faculty, events, and more to find them here.

+ +
+ {% endif %} + +
+
+ + +
+
+
+{% endblock %} diff --git a/sites/osu/templates/admissions.html b/sites/osu/templates/admissions.html new file mode 100644 index 00000000..ddb2786a --- /dev/null +++ b/sites/osu/templates/admissions.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} +{% block title %}Admissions — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('admissions-visit') %} +
{{ banner.alt }}
+ + +
+
+
{{ undergrad_programs }}
+
Undergraduate Programs
+
+
+
{{ grad_programs }}
+
Graduate Programs
+
+
+
{{ online_programs }}
+
Online Programs
+
+
+ + +
+

Undergraduate Admissions

+
+
+

Ohio State welcomes applications from students across Ohio, the nation, and the world. Our comprehensive admissions process evaluates academic achievement, personal character, and potential for success.

+

Application Requirements

+
    +
  • Completed online application
  • +
  • Official high school transcripts
  • +
  • ACT or SAT scores (optional for 2024-2025)
  • +
  • Letters of recommendation
  • +
  • Personal essay
  • +
  • $60 application fee
  • +
+ +
+
+

Key Dates

+ + + + + +
Early Action DeadlineNovember 1
Regular Decision DeadlineFebruary 1
Notification DateMarch – April
Enrollment Deposit DueMay 1
+
+
Middle 50% GPA: 3.6–4.0
+
Middle 50% ACT: 27–33
+
Middle 50% SAT: 1270–1490
+
+
+
+
+ + +
+

Graduate Admissions

+
+
+

Ohio State's Graduate School offers master's, doctoral, and professional degree programs in virtually every field. Applications are reviewed by individual programs.

+

General Requirements

+
    +
  • Bachelor's degree from an accredited institution
  • +
  • Minimum undergraduate GPA of 3.0
  • +
  • Official transcripts from all institutions
  • +
  • Statement of purpose
  • +
  • Letters of recommendation (typically 3)
  • +
  • GRE scores (program-specific)
  • +
  • English proficiency for international applicants
  • +
+ +
+ +
+
+ + +
+

Financial Aid & Scholarships

+

Ohio State is committed to making a world-class education accessible. Over 60% of students receive some form of financial assistance.

+ +
+{% endblock %} diff --git a/sites/osu/templates/athletics.html b/sites/osu/templates/athletics.html new file mode 100644 index 00000000..6b0ce7ba --- /dev/null +++ b/sites/osu/templates/athletics.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% block title %}Buckeye Athletics — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('athletics-football') %} +
{{ banner.alt }}
+ +
+
+
{{ teams|length }}Featured Teams
+
{{ men_teams|length }}Men's Teams
+
{{ women_teams|length }}Women's Teams
+
Big TenConference
+
+
+ +{% if men_teams %} +
+

Men's Sports

+
+ {% for team in men_teams %} + {% set team_photo = athletics_image(team) %} +
+ {% if team_photo %}{{ team_photo.alt }}{% endif %} +
+
+
+ {{ team.sport[:2].upper() }} +
+
+ +
{{ team.sport }}
+ {% if team.recent_record %}
Record: {{ team.recent_record }}
{% endif %} +
+
+ {% if team.national_titles > 0 %} +
🏆 {{ team.national_titles }} National Title{% if team.national_titles > 1 %}s{% endif %}
+ {% endif %} +
+
+ {% endfor %} +
+
+{% endif %} + +{% if women_teams %} +
+

Women's Sports

+
+ {% for team in women_teams %} + {% set team_photo = athletics_image(team) %} +
+ {% if team_photo %}{{ team_photo.alt }}{% endif %} +
+
+
+ {{ team.sport[:2].upper() }} +
+
+ +
{{ team.sport }}
+ {% if team.recent_record %}
Record: {{ team.recent_record }}
{% endif %} +
+
+ {% if team.national_titles > 0 %} +
🏆 {{ team.national_titles }} National Title{% if team.national_titles > 1 %}s{% endif %}
+ {% endif %} +
+
+ {% endfor %} +
+
+{% endif %} + +{% if coed_teams %} +
+

Co-ed Sports

+
+ {% for team in coed_teams %} +
+
+ +
{{ team.sport }}
+
+
+ {% endfor %} +
+
+{% endif %} +{% endblock %} diff --git a/sites/osu/templates/athletics_team.html b/sites/osu/templates/athletics_team.html new file mode 100644 index 00000000..b49b93ea --- /dev/null +++ b/sites/osu/templates/athletics_team.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} +{% block title %}{{ team.name }} — Ohio State Athletics{% endblock %} +{% block content %} + + +{% set team_photo = athletics_image(team) %} +
+
+ {% if team_photo %}{{ team_photo.alt }}{% endif %} +
+
+ {{ team.sport[:2].upper() }} +
+
+

{{ team.name }}

+

{{ team.sport }} • {{ team.gender }}

+

{{ team.conference }}

+
+
+ + {% if team.national_titles > 0 %} +
+ 🏆 + {{ team.national_titles }} National Championship{% if team.national_titles > 1 %}s{% endif %} +
+ {% endif %} + +
+
+ {% if team.coach %} +
Head Coach
{{ team.coach }}
+ {% endif %} + {% if team.home_venue %} +
Home Venue
{{ team.home_venue }}
+ {% endif %} + {% if team.recent_record %} +
Recent Record
{{ team.recent_record }}
+ {% endif %} +
Conference
{{ team.conference }}
+
+
+ +
+

About {{ team.name }}

+

+ The Ohio State {{ team.name }} compete in the {{ team.conference }} conference in {{ team.sport }}. + {% if team.coach %}The team is led by Head Coach {{ team.coach }}.{% endif %} + {% if team.home_venue %}Home games are played at {{ team.home_venue }}.{% endif %} + {% if team.national_titles > 0 %}The Buckeyes have won {{ team.national_titles }} national championship{% if team.national_titles > 1 %}s{% endif %} in this sport.{% endif %} +

+
+ + {% if current_user.is_authenticated %} +
+ + + + + +
+ {% endif %} +
+ + +
+{% endblock %} diff --git a/sites/osu/templates/base.html b/sites/osu/templates/base.html new file mode 100644 index 00000000..953cbf56 --- /dev/null +++ b/sites/osu/templates/base.html @@ -0,0 +1,287 @@ + + + + + + + {% block title %}The Ohio State University{% endblock %} + + {% block extra_head %}{% endblock %} + + + + +
+
+ The Ohio State University — Columbus, Ohio 43210 +
+ {% if current_user.is_authenticated %} + My Account +
+ + +
+ {% else %} + Sign In + Register + {% endif %} + Search +
+
+
+ + + + + + + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + +
+ {% block content %}{% endblock %} +
+ + + + + {% block extra_js %}{% endblock %} + + diff --git a/sites/osu/templates/department_detail.html b/sites/osu/templates/department_detail.html new file mode 100644 index 00000000..627166d2 --- /dev/null +++ b/sites/osu/templates/department_detail.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} +{% block title %}{{ dept.name }} — Ohio State{% endblock %} +{% block content %} + + +{% set department_photo = image_asset('academics-undergraduate') %} +
+
+ {{ department_photo.alt }} +

{{ dept.name }}

+ {% if dept.college %}

{{ dept.college.name }}

{% endif %} + +
+
+ {% for paragraph in dept.description.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} +
+
+ + {% if programs %} +
+

Degree Programs

+
+ {% for prog in programs %} +
+
+ {{ prog.degree_type }} + {% if prog.is_online %}Online{% endif %} + +
{{ prog.units }} credits • {{ prog.duration_years }} years
+
+
+ {% endfor %} +
+
+ {% endif %} + + {% if faculty_list %} +
+

Faculty

+
+ {% for member in faculty_list %} +
+
+ +
{{ member.title }}
+ {% if member.email %}{% endif %} + {% if member.research_interests %}
{{ member.research_interests[:80] }}
{% endif %} +
+
+ {% endfor %} +
+
+ {% endif %} +
+ + +
+{% endblock %} diff --git a/sites/osu/templates/departments.html b/sites/osu/templates/departments.html new file mode 100644 index 00000000..8ef8cf69 --- /dev/null +++ b/sites/osu/templates/departments.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}Departments — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('academics-undergraduate') %} +
{{ banner.alt }}
+ +{% for college, depts in depts_by_college.items() %} +{% if depts %} +
+

+ {{ college.name }} + {% if college.dean %}Dean: {{ college.dean }}{% endif %} +

+
+ {% for dept in depts %} +
+
+

{{ dept.name }}

+ {% if dept.chair %}
Chair: {{ dept.chair }}
{% endif %} + {% if dept.location %}
📍 {{ dept.location }}
{% endif %} +

{{ dept.description[:100] }}{% if dept.description|length > 100 %}...{% endif %}

+ +
+
+ {% endfor %} +
+
+{% endif %} +{% endfor %} +{% endblock %} diff --git a/sites/osu/templates/event_detail.html b/sites/osu/templates/event_detail.html new file mode 100644 index 00000000..5d3f8749 --- /dev/null +++ b/sites/osu/templates/event_detail.html @@ -0,0 +1,90 @@ +{% extends "base.html" %} +{% block title %}{{ event.title }} — Ohio State Events{% endblock %} +{% block content %} + + +{% set event_photo = image_asset('campus-life') %} +
+
+ {{ event_photo.alt }} + {{ event.category }} + {% if event.registration_required %}Registration Required{% endif %} +

{{ event.title }}

+ +
+
+
+
Date & Time
+
{{ event.start_datetime.strftime('%A, %B %d, %Y') }}
+
{{ event.start_datetime.strftime('%I:%M %p') }}{% if event.end_datetime %} – {{ event.end_datetime.strftime('%I:%M %p') }}{% endif %}
+
+ {% if event.location %} +
+
Location
+
{{ event.location }}
+ {% if event.building %}
{{ event.building }}
{% endif %} +
{{ event.campus }} Campus
+
+ {% endif %} + {% if event.organizer %} +
+
Organizer
+
{{ event.organizer }}
+
+ {% endif %} +
+
Cost
+
{{ event.cost or 'Free' }}
+
+
+
+ +
+

About This Event

+
+ {% for paragraph in event.description.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} +
+
+ + {% if current_user.is_authenticated %} +
+ + + + + +
+ {% endif %} +
+ + +
+{% endblock %} diff --git a/sites/osu/templates/events.html b/sites/osu/templates/events.html new file mode 100644 index 00000000..c98ec377 --- /dev/null +++ b/sites/osu/templates/events.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% block title %}Events — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('campus-life') %} +
{{ banner.alt }}
+ + +{% endblock %} diff --git a/sites/osu/templates/faculty.html b/sites/osu/templates/faculty.html new file mode 100644 index 00000000..42708660 --- /dev/null +++ b/sites/osu/templates/faculty.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} +{% block title %}Faculty — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('about-education') %} +
{{ banner.alt }}
+ + +{% endblock %} diff --git a/sites/osu/templates/faculty_profile.html b/sites/osu/templates/faculty_profile.html new file mode 100644 index 00000000..0dd00e89 --- /dev/null +++ b/sites/osu/templates/faculty_profile.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} +{% block title %}{{ member.name }} — Ohio State Faculty{% endblock %} +{% block content %} + + +{% set faculty_photo = image_asset('about-education') %} +
+
+ {{ faculty_photo.alt }} +
+
+ {{ member.name[0] }} +
+
+

{{ member.name }}

+

{{ member.title }}

+ {% if member.department %}

{{ member.department.name }}{% if member.department.college %}, {{ member.department.college.name }}{% endif %}

{% endif %} + {% if member.is_emeritus %}Professor Emeritus{% endif %} +
+
+ +
+
+ {% if member.email %} + + {% endif %} + {% if member.phone %} +
Phone
{{ member.phone }}
+ {% endif %} + {% if member.office %} +
Office
{{ member.office }}
+ {% endif %} +
+
+ + {% if member.research_interests %} +
+

Research Interests

+
+ {% for interest in member.research_interests.split(',') %} + {{ interest.strip() }} + {% endfor %} +
+
+ {% endif %} + + {% if member.bio %} +
+

Biography

+
+ {% for paragraph in member.bio.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} +
+
+ {% endif %} + + {% if current_user.is_authenticated %} +
+ + + + + +
+ {% endif %} +
+ + +
+{% endblock %} diff --git a/sites/osu/templates/index.html b/sites/osu/templates/index.html new file mode 100644 index 00000000..fb08fb1e --- /dev/null +++ b/sites/osu/templates/index.html @@ -0,0 +1,137 @@ +{% extends "base.html" %} +{% block title %}The Ohio State University{% endblock %} +{% block content %} +{% set hero_image = image_asset('home-hero') %} +
+ {{ hero_image.alt }} +
+

The Ohio State University

+

A top-20 public research university and home to over 60,000 students and world-class faculty. Go Bucks!

+ +
+
+ + +
+
+
{{ stats.undergrad_count | default(46820) | int }}Undergraduates
+
{{ stats.grad_count | default(14000) | int }}Graduate Students
+
{{ stats.degree_programs | default(500) }}Degree Programs
+
{{ stats.faculty_count | default(7000) | int }}Faculty
+
#{{ stats.fulbright_rank | default(1) }}Fulbright Scholars
+
${{ stats.research_expenditure | default(1.3) }}BResearch Expenditure
+
{{ stats.campuses | default(6) }}Campuses
+
+
+ + +{% if featured_news %} +
+

Latest News

+
+ {% for article in featured_news[:3] %} + {% set article_image = news_image(article) %} +
+ {{ article_image.alt }} +
+
+ {{ article.category }} + {{ article.published_date.strftime('%B %d, %Y') }} +
+ +

{{ article.summary[:150] }}{% if article.summary|length > 150 %}...{% endif %}

+ +
+
+ {% endfor %} +
+ +
+{% endif %} + + +{% if upcoming_events %} +
+

Upcoming Events

+
+ {% for event in upcoming_events %} +
+
+
+
+
{{ event.start_datetime.strftime('%b') }}
+
{{ event.start_datetime.strftime('%d') }}
+
+
+ +
{{ event.start_datetime.strftime('%I:%M %p') }} • {{ event.location }}
+
{{ event.category }}
+
+
+
+
+ {% endfor %} +
+ +
+{% endif %} + + +{% if recent_research %} +
+

Research Excellence

+
+ {% for center in recent_research %} + {% set center_image = research_image(center) %} +
+ {{ center_image.alt }} +
+ +

{{ center.description[:100] }}{% if center.description|length > 100 %}...{% endif %}

+
+
+ {% endfor %} +
+ +
+{% endif %} + + +
+

Explore Ohio State

+ +
+{% endblock %} diff --git a/sites/osu/templates/login.html b/sites/osu/templates/login.html new file mode 100644 index 00000000..b22a91a7 --- /dev/null +++ b/sites/osu/templates/login.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}Sign In — Ohio State{% endblock %} +{% block content %} +
+
+
+

Sign In

+

Access your Ohio State account

+
+ +
+
+
+ {{ form.hidden_tag() }} + +
+ {{ form.email.label }} + {{ form.email(placeholder='your.name@osu.edu', autocomplete='email') }} + {% for error in form.email.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ {{ form.password.label }} + {{ form.password(placeholder='Password', autocomplete='current-password') }} + {% for error in form.password.errors %} +
{{ error }}
+ {% endfor %} +
+ + +
+ +
+ Don't have an account? Register here +
+
+
+ +
+ Use the account credentials supplied by your assigned task. +
+
+{% endblock %} diff --git a/sites/osu/templates/news.html b/sites/osu/templates/news.html new file mode 100644 index 00000000..6ce74c92 --- /dev/null +++ b/sites/osu/templates/news.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} +{% block title %}News — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('news-campus') %} +
{{ banner.alt }}
+ + +{% endblock %} diff --git a/sites/osu/templates/news_article.html b/sites/osu/templates/news_article.html new file mode 100644 index 00000000..462b191d --- /dev/null +++ b/sites/osu/templates/news_article.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} +{% block title %}{{ article.title }} — Ohio State News{% endblock %} +{% block content %} + + +{% set article_photo = news_image(article) %} + +{% endblock %} diff --git a/sites/osu/templates/program_detail.html b/sites/osu/templates/program_detail.html new file mode 100644 index 00000000..f42fba17 --- /dev/null +++ b/sites/osu/templates/program_detail.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} +{% block title %}{{ program.name }} — Ohio State{% endblock %} +{% block content %} + + +{% set program_photo = image_asset('academics-graduate' if program.degree_type in ['MA', 'MS', 'MBA', 'MPH', 'PhD'] else 'academics-undergraduate') %} +
+
+ {{ program_photo.alt }} +
+ {{ program.degree_type }} + {% if program.is_online %}Online Available{% endif %} +
+

{{ program.name }}

+ {% if program.college %}

{{ program.college.name }}

{% endif %} + {% if program.department %}

Department of {{ program.department.name }}

{% endif %} + +
+

Program Overview

+
+ {% for paragraph in program.description.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} +
+
+ + {% if program.requirements %} +
+

Requirements

+
+ {% for line in program.requirements.split('\n') %} + {% if line.strip() %}

{{ line }}

{% endif %} + {% endfor %} +
+
+ {% endif %} + + {% if related %} +
+

Related Programs

+
+ {% for rel in related %} +
+
+ {{ rel.degree_type }} + +
+
+ {% endfor %} +
+
+ {% endif %} +
+ + +
+{% endblock %} diff --git a/sites/osu/templates/programs.html b/sites/osu/templates/programs.html new file mode 100644 index 00000000..b03bc49d --- /dev/null +++ b/sites/osu/templates/programs.html @@ -0,0 +1,100 @@ +{% extends "base.html" %} +{% block title %}Degree Programs — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('academics-graduate') %} +
{{ banner.alt }}
+ + +{% endblock %} diff --git a/sites/osu/templates/register.html b/sites/osu/templates/register.html new file mode 100644 index 00000000..12a2912a --- /dev/null +++ b/sites/osu/templates/register.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block title %}Register — Ohio State{% endblock %} +{% block content %} +
+
+
+

Create Account

+

Join The Ohio State University community

+
+ +
+
+
+ {{ form.hidden_tag() }} + +
+ {{ form.username.label }} + {{ form.username(placeholder='buckeyefan', autocomplete='username') }} + {% for error in form.username.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ {{ form.full_name.label }} + {{ form.full_name(placeholder='Jane Doe', autocomplete='name') }} + {% for error in form.full_name.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ {{ form.email.label }} + {{ form.email(placeholder='your.name@osu.edu', autocomplete='email') }} + {% for error in form.email.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ {{ form.password.label }} + {{ form.password(placeholder='At least 8 characters', autocomplete='new-password') }} + {% for error in form.password.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ {{ form.confirm.label }} + {{ form.confirm(placeholder='Repeat password', autocomplete='new-password') }} + {% for error in form.confirm.errors %} +
{{ error }}
+ {% endfor %} +
+ + +
+ +
+ Already have an account? Sign in +
+
+
+
+{% endblock %} diff --git a/sites/osu/templates/research.html b/sites/osu/templates/research.html new file mode 100644 index 00000000..60d23b8c --- /dev/null +++ b/sites/osu/templates/research.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} +{% block title %}Research — Ohio State{% endblock %} +{% block content %} + +{% set banner = image_asset('research-hero') %} +
{{ banner.alt }}
+ +
+
+
$1.3BAnnual Research Expenditure
+
{{ centers|length }}Research Centers & Institutes
+
#1Fulbright Scholars Producing Institution
+
88Extension County Offices
+
+
+ +

Research Centers & Institutes

+
+ {% for center in centers %} + {% set center_photo = research_image(center) %} +
+ {{ center_photo.alt }} +
+

{{ center.name }}

+ {% if center.college %}
{{ center.college.name }}
{% endif %} +

{{ center.description[:160] }}{% if center.description|length > 160 %}...{% endif %}

+ {% if center.focus_areas %} +
+ {% for area in center.focus_areas.split(',')[:3] %} + {{ area.strip() }} + {% endfor %} +
+ {% endif %} +
+ {% if center.director %}Director: {{ center.director }}
{% endif %} + Founded {{ center.founded_year }} +
+ +
+
+ {% endfor %} +
+ +{% if colleges %} +
+

Research by College

+
+ {% for college in colleges %} + {% if college.research_centers %} +
+

{{ college.name }}

+ {% for rc in college.research_centers %} + {{ rc.name }} + {% endfor %} +
+ {% endif %} + {% endfor %} +
+
+{% endif %} +{% endblock %} diff --git a/sites/osu/templates/research_center.html b/sites/osu/templates/research_center.html new file mode 100644 index 00000000..a712f672 --- /dev/null +++ b/sites/osu/templates/research_center.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} +{% block title %}{{ center.name }} — Ohio State Research{% endblock %} +{% block content %} + + +{% set center_photo = research_image(center) %} +
+
+ {{ center_photo.alt }} +

{{ center.name }}

+ {% if center.college %}

{{ center.college.name }}

{% endif %} + + {% if center.focus_areas %} +
+ {% for area in center.focus_areas.split(',') %} + {{ area.strip() }} + {% endfor %} +
+ {% endif %} + +
+

About This Center

+
+ {% for paragraph in center.description.split('\n\n') %} +

{{ paragraph }}

+ {% endfor %} +
+
+ + {% if current_user.is_authenticated %} +
+ + + + + +
+ {% endif %} + + {% if related %} +
+

Related Centers

+
+ {% for rel in related %} +
+
+ +

{{ rel.description[:100] }}...

+
+
+ {% endfor %} +
+
+ {% endif %} +
+ + +
+{% endblock %} diff --git a/sites/osu/templates/search.html b/sites/osu/templates/search.html new file mode 100644 index 00000000..72854e31 --- /dev/null +++ b/sites/osu/templates/search.html @@ -0,0 +1,141 @@ +{% extends "base.html" %} +{% block title %}Search — Ohio State{% endblock %} +{% block content %} +

Search Ohio State

+ +
+ + +
+ +{% if q %} +

+ Found {{ total }} result{% if total != 1 %}s{% endif %} for “{{ q }}” +

+ +{% if results.programs %} +
+

Degree Programs ({{ results.programs|length }})

+
+ {% for prog in results.programs %} +
+
+ {{ prog.degree_type }} + + {% if prog.college %}
{{ prog.college.name }}
{% endif %} +
+
+ {% endfor %} +
+ +
+{% endif %} + +{% if results.news %} +
+

News ({{ results.news|length }})

+ {% for article in results.news %} +
+
{{ article.category }} {{ article.published_date.strftime('%b %d, %Y') }}
+ {{ article.title }} +

{{ article.summary[:150] }}{% if article.summary|length > 150 %}...{% endif %}

+
+ {% endfor %} + +
+{% endif %} + +{% if results.faculty %} +
+

Faculty ({{ results.faculty|length }})

+
+ {% for member in results.faculty %} +
+
+ +
{{ member.title }}
+ {% if member.department %}
{{ member.department.name }}
{% endif %} +
+
+ {% endfor %} +
+ +
+{% endif %} + +{% if results.events %} +
+

Events ({{ results.events|length }})

+ {% for event in results.events %} +
+
+
+
{{ event.start_datetime.strftime('%b') }}
+
{{ event.start_datetime.strftime('%d') }}
+
+
+ {{ event.title }} +
{{ event.location }} • {{ event.category }}
+
+
+
+ {% endfor %} + +
+{% endif %} + +{% if results.research %} +
+

Research Centers ({{ results.research|length }})

+
+ {% for center in results.research %} +
+
+ +

{{ center.description[:100] }}...

+
+
+ {% endfor %} +
+
+{% endif %} + +{% if results.athletics %} +
+

Athletics ({{ results.athletics|length }})

+
+ {% for team in results.athletics %} +
+
+ +
{{ team.sport }} • {{ team.gender }}
+
+
+ {% endfor %} +
+
+{% endif %} + +{% if total == 0 %} +
+
🔍
+

No results found

+

Try different keywords or browse our site using the navigation above.

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

What are you looking for?

+

Search across programs, news, faculty, events, research, and athletics.

+ +
+{% endif %} +{% endblock %} diff --git a/sites/osu/verify/README.md b/sites/osu/verify/README.md new file mode 100644 index 00000000..6e9a37ad --- /dev/null +++ b/sites/osu/verify/README.md @@ -0,0 +1,17 @@ +# Ohio State verifier suite + +The OSU mirror has 20 deterministic task verifiers, one per row in `sites/osu/tasks.jsonl`. + +Each verifier requires the expected task ID, a non-empty answer, same-origin navigation on the configured loopback origin, the task-specific path/query/click sequence, affirmative answer facts bound to the requested entity, and a complete unchanged SQLite database. All current OSU tasks are read-only. + +Run the regression suite from the repository root: + +```bash +uv run --with Flask==3.1.0 --with Flask-SQLAlchemy==3.1.1 --with Flask-WTF==1.2.2 --with Flask-Bcrypt==1.0.1 --with Flask-Login==0.6.3 --with email-validator==2.2.0 python -m unittest sites.osu.verify.test_verifiers sites.osu.verify.test_environment_quality sites.osu.verify.test_app -v +``` + +The tests include positive cases and controls for answer-only runs, wrong task IDs, external-origin URLs, database mutations, missing filters, negated answers, swapped comparison values, and missing or altered image files. + +## Image assets + +The mirror uses 19 photographs crawled from official Ohio State web properties. `sites/osu/image_sources.json` records each source page, source URL, dimensions, alt text, and source/output SHA-256. `sites/osu/fetch_images.py` reproduces the normalized WebP files. The binaries are distributed as `osu.tar.gz` from the Hugging Face asset revision pinned in `.assets-revision`. diff --git a/sites/osu/verify/TASK_REVIEW.md b/sites/osu/verify/TASK_REVIEW.md new file mode 100644 index 00000000..b976eb98 --- /dev/null +++ b/sites/osu/verify/TASK_REVIEW.md @@ -0,0 +1,28 @@ +# OSU task review + +All 20 tasks were re-grounded against the tracked seed source and the generated SQLite seed. Task URLs use OSU's current site index 20 and port `40020` after integration with current main. + +| Task | Required visible workflow | Ground truth | +|---:|---|---| +| 0 | Academics | Fisher dean: Anil Makhija | +| 1 | About | Varsity Sports displayed: 36 | +| 2 | Athletics and two team details | Football and wrestling: Big Ten | +| 3 | Athletics to football | Ryan Day; 11-2 | +| 4 | Search to expenditure article | $1.3 billion; September 23, 2024 | +| 5 | About | 1870; Ohio Agricultural and Mechanical College | +| 6 | Research to TDAI | Beth Plale; four focus areas | +| 7 | About | 46,820; 14,000; difference 32,820 | +| 8 | Academics comparison | Engineering 8,000; Fisher 4,500; difference 3,500 | +| 9 | Engineering programs filter | BS, MS, PhD; three types | +| 10 | Athletics to wrestling | Tom Ryan; Covelli Center | +| 11 | Research to OSC | David Bickel; 1987 | +| 12 | Programs search to JD | JD; 90 credits; 3 years | +| 13 | Departments to Mathematics | James Cogdell; 100 Mathematics Building | +| 14 | Athletics and two team details | Football: Ohio Stadium; men's basketball: Value City Arena | +| 15 | Athletics comparison | Wrestling 8; fencing 2; difference 6 | +| 16 | MBA degree filter and detail | April 1; 60 credits; GRE not required | +| 17 | Search to cancer article | Exact title; Jody Sheridan | +| 18 | Research to James | William Farrar; four focus areas | +| 19 | Research to Clean Hydrogen | Yann Guezennec; 2022; four focus areas | + +Read-only task verification compares every non-SQLite internal table before and after execution. Navigation checks parse URLs and require the same loopback origin, exact normalized paths, exact required query values, operation order, and visible-link click transitions where requested. diff --git a/sites/osu/verify/selfcheck.py b/sites/osu/verify/selfcheck.py new file mode 100644 index 00000000..8350f2ce --- /dev/null +++ b/sites/osu/verify/selfcheck.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Run the complete OSU verifier, application, and environment regression suite.""" +from __future__ import annotations +import sys +import unittest +from pathlib import Path + +REPO_ROOT=Path(__file__).resolve().parents[3] +sys.path.insert(0,str(REPO_ROOT)) + +if __name__=='__main__': + suite=unittest.defaultTestLoader.loadTestsFromNames([ + 'sites.osu.verify.test_verifiers', + 'sites.osu.verify.test_environment_quality', + 'sites.osu.verify.test_app', + ]) + result=unittest.TextTestRunner(verbosity=2).run(suite) + raise SystemExit(0 if result.wasSuccessful() else 1) diff --git a/sites/osu/verify/test_app.py b/sites/osu/verify/test_app.py new file mode 100644 index 00000000..6b849dd0 --- /dev/null +++ b/sites/osu/verify/test_app.py @@ -0,0 +1,57 @@ +"""HTTP regression tests for the OSU mirror.""" +from __future__ import annotations +import importlib,os,re,shutil,sqlite3,sys,unittest +from pathlib import Path +from sites.osu.verify.test_support import ensure_seed +SITE=Path(__file__).resolve().parents[1];SEED=ensure_seed();RUNTIME=SITE/'instance/osu.db' +def csrf(response): + m=re.search(rb'name="csrf_token"[^>]*value="([^"]+)"|value="([^"]+)"[^>]*name="csrf_token"',response.data);assert m,response.request.path;return (m.group(1) or m.group(2)).decode() +class AppTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + shutil.rmtree(SITE/'instance',ignore_errors=True);(SITE/'instance').mkdir();shutil.copy2(SEED,RUNTIME);os.environ['OSU_SECRET_KEY']='test-only';sys.path.insert(0,str(SITE));cls.module=importlib.import_module('app');cls.app=cls.module.app;cls.app.config.update(TESTING=True) + @classmethod + def tearDownClass(cls): + with cls.app.app_context():cls.module.db.session.remove() + shutil.rmtree(SITE/'instance',ignore_errors=True) + def setUp(self):self.client=self.app.test_client() + def snap(self): + c=sqlite3.connect(RUNTIME) + try: + ts=[r[0] for r in c.execute("select name from sqlite_master where type='table' and name not like 'sqlite_%' order by name")];return {t:c.execute(f'select * from "{t}" order by rowid').fetchall() for t in ts} + finally:c.close() + def login(self,next_url=None): + route='/login'+(f'?next={next_url}' if next_url else '');p=self.client.get(route);return self.client.post(route,data={'csrf_token':csrf(p),'email':'alice@osu.edu','password':'test1234'},follow_redirects=False) + def test_all_routes_render(self): + paths=['/','/about','/academics','/programs','/programs?college=engineering','/research','/departments','/faculty','/news','/events','/athletics','/admissions','/search?q=cancer+research','/login','/register','/_health'] + with self.app.app_context(): + paths += [f'/programs/{x.slug}' for x in self.module.Program.query.all()];paths += [f'/research/{x.slug}' for x in self.module.ResearchCenter.query.all()];paths += [f'/departments/{x.slug}' for x in self.module.Department.query.all()];paths += [f'/faculty/{x.slug}' for x in self.module.Faculty.query.all()];paths += [f'/news/{x.slug}' for x in self.module.NewsArticle.query.all()];paths += [f'/events/{x.id}' for x in self.module.Event.query.all()];paths += [f'/athletics/{x.slug}' for x in self.module.AthleticTeam.query.all()] + for path in paths: + with self.subTest(path=path):self.assertEqual(self.client.get(path).status_code,200) + def test_real_image_assets_are_served(self): + import json + for item in json.loads((SITE/'image_sources.json').read_text())['images']: + with self.subTest(file=item['file']): + response=self.client.get('/static/images/'+item['file']);self.assertEqual(response.status_code,200);self.assertEqual(response.mimetype,'image/webp');self.assertGreater(len(response.get_data()),5000);response.close() + def test_get_routes_are_read_only(self): + before=self.snap() + for p in ('/','/news/ohio-state-researchers-develop-breakthrough-cancer-immunotherapy','/about','/search?q=cancer+research','/events'):self.assertEqual(self.client.get(p).status_code,200) + self.assertEqual(before,self.snap()) + def test_logout_and_csrf(self): + self.assertEqual(self.client.get('/logout').status_code,405) + for p in ('/logout','/bookmark/add','/bookmark/remove'): + with self.subTest(p=p):self.assertEqual(self.client.post(p).status_code,400) + def test_open_redirect_rejected_and_login_works(self): + r=self.login('//evil.invalid');self.assertEqual(r.status_code,302);self.assertEqual(r.headers['Location'],'/') + def test_invalid_bookmarks_rejected(self): + self.login();p=self.client.get('/programs/juris-doctor-jd');tok=csrf(p) + self.assertEqual(self.client.post('/bookmark/add',data={'csrf_token':tok,'item_type':'invalid','item_id':'1'}).status_code,400) + p=self.client.get('/programs/juris-doctor-jd');self.assertEqual(self.client.post('/bookmark/add',data={'csrf_token':csrf(p),'item_type':'program','item_id':'99999'}).status_code,404) + def test_valid_bookmark_and_duplicate_are_single_row(self): + self.login();p=self.client.get('/programs/juris-doctor-jd');data={'csrf_token':csrf(p),'item_type':'program','item_id':'14','next':'/programs/juris-doctor-jd'};self.assertEqual(self.client.post('/bookmark/add',data=data).status_code,302) + p=self.client.get('/programs/juris-doctor-jd');data['csrf_token']=csrf(p);self.assertEqual(self.client.post('/bookmark/add',data=data).status_code,302) + c=sqlite3.connect(RUNTIME);self.assertEqual(c.execute("select count(*) from bookmarks where user_id=1 and item_type='program' and item_id=14").fetchone()[0],1);c.close() + def test_upcoming_events_visible_with_fixed_clock(self): + body=self.client.get('/events').get_data(as_text=True);self.assertIn('Buckeyes vs. Michigan State Football',body);self.assertIn('CFAES Annual Farm Science Review',body) + def test_request_size_limit(self):self.assertEqual(self.client.post('/register',data=b'x'*(65*1024),content_type='application/x-www-form-urlencoded').status_code,413) +if __name__=='__main__':unittest.main() diff --git a/sites/osu/verify/test_environment_quality.py b/sites/osu/verify/test_environment_quality.py new file mode 100644 index 00000000..54c61b1b --- /dev/null +++ b/sites/osu/verify/test_environment_quality.py @@ -0,0 +1,46 @@ +"""Static and seed-quality regressions for the OSU mirror.""" +from __future__ import annotations +import hashlib,json,shutil,sqlite3,subprocess,sys,tempfile,unittest +from urllib.parse import urlparse +from pathlib import Path +from sites.osu.verify.test_support import ensure_seed +SITE=Path(__file__).resolve().parents[1];ROOT=SITE.parents[1];SEED=ensure_seed() +class EnvironmentTests(unittest.TestCase): + def test_site_registration_and_task_manifest(self): + self.assertIn('ted osu)',(ROOT/'websyn_start.sh').read_text());self.assertIn("'ted', 'osu'",(ROOT/'control_server.py').read_text());self.assertIn('40000-40020',(ROOT/'Dockerfile').read_text()) + rows=[json.loads(x) for x in (SITE/'tasks.jsonl').read_text().splitlines()];self.assertEqual(len(rows),20) + for i,r in enumerate(rows):self.assertEqual(r['id'],f'Ohio State University--{i}');self.assertEqual(r['web'],'http://localhost:40020/');self.assertTrue((ROOT/r['verifier_path']).is_file());self.assertNotIn('answer',r) + def test_real_image_manifest_and_files(self): + manifest=json.loads((SITE/'image_sources.json').read_text())['images'];self.assertGreaterEqual(len(manifest),19) + allowed_pages={'www.osu.edu','undergrad.osu.edu','fisher.osu.edu','ohiostatebuckeyes.com','cancer.osu.edu','news.osu.edu'} + references=(SITE/'app.py').read_text()+''.join(path.read_text() for path in (SITE/'templates').glob('*.html')) + for item in manifest: + with self.subTest(file=item['file']): + image=SITE/'static/images'/item['file'];self.assertTrue(image.is_file());self.assertGreater(image.stat().st_size,5000);self.assertEqual(hashlib.sha256(image.read_bytes()).hexdigest(),item['output_sha256']);self.assertEqual(image.suffix,'.webp');self.assertIn(urlparse(item['source_page']).hostname,allowed_pages);self.assertTrue(item['alt'].strip());self.assertIn(item['file'].removesuffix('.webp'),references) + def test_seed_counts_and_constraints(self): + c=sqlite3.connect(SEED) + try: + expected={'colleges':16,'departments':15,'programs':20,'news_articles':20,'events':16,'research_centers':15,'faculty':15,'athletic_teams':26,'users':4,'bookmarks':0} + self.assertEqual({t:c.execute(f'select count(*) from {t}').fetchone()[0] for t in expected},expected) + indexes={r[1] for r in c.execute('pragma index_list(bookmarks)')};self.assertTrue(any('bookmark' in x for x in indexes),indexes) + finally:c.close() + def test_seed_generation_is_byte_deterministic(self): + hashes=[] + with tempfile.TemporaryDirectory(prefix='osu-seed-') as tmp: + for n in (1,2): + d=Path(tmp)/str(n);d.mkdir();shutil.copy2(SITE/'app.py',d/'app.py');shutil.copy2(SITE/'seed_data.py',d/'seed_data.py');shutil.copy2(SITE/'image_sources.json',d/'image_sources.json') + subprocess.run([sys.executable,'-c','import app'],cwd=d,check=True,capture_output=True,text=True);database=d/'instance/osu.db';hashes.append(hashlib.sha256(database.read_bytes()).hexdigest()) + self.assertEqual(hashes[0],hashes[1]) + def test_post_forms_have_csrf(self): + missing=[] + for p in (SITE/'templates').glob('*.html'): + lines=p.read_text().splitlines() + for i,line in enumerate(lines): + if '",base) + for name in ('athletics_team.html','event_detail.html','faculty_profile.html','program_detail.html','research_center.html','department_detail.html'):self.assertIn('class="detail-layout"',(SITE/'templates'/name).read_text(),name) + def test_all_read_only_verifiers_compare_complete_database(self): + for i in range(20):self.assertIn('check_read_only',(SITE/f'verify/verify_{i}.py').read_text(),i) +if __name__=='__main__':unittest.main() diff --git a/sites/osu/verify/test_support.py b/sites/osu/verify/test_support.py new file mode 100644 index 00000000..0cfbd72a --- /dev/null +++ b/sites/osu/verify/test_support.py @@ -0,0 +1,20 @@ +"""Shared test setup for generated OSU seed assets.""" +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +SITE = Path(__file__).resolve().parents[1] +SEED = SITE / 'instance_seed' / 'osu.db' + + +def ensure_seed(): + if SEED.is_file(): + return SEED + subprocess.run([sys.executable, '-c', 'import app'], cwd=SITE, check=True, capture_output=True, text=True) + SEED.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(SITE / 'instance' / 'osu.db', SEED) + shutil.rmtree(SITE / 'instance') + return SEED diff --git a/sites/osu/verify/test_verifiers.py b/sites/osu/verify/test_verifiers.py new file mode 100644 index 00000000..100efa64 --- /dev/null +++ b/sites/osu/verify/test_verifiers.py @@ -0,0 +1,76 @@ +"""Positive and adversarial tests for all OSU verifiers.""" +from __future__ import annotations +import json,shutil,sqlite3,subprocess,sys,tempfile,unittest +from pathlib import Path +from sites.osu.verify.test_support import ensure_seed +VERIFY_DIR=Path(__file__).resolve().parent;SEED=ensure_seed();BASE='http://localhost:40020' +def url(p):return BASE+p +def nav(p):return {'url':url(p),'action':'navigate','params':{}} +def click(a,b):return {'url':url(a),'url_after':url(b),'action':'click','params':{}} +def trans(a,b):return [click(a,b),nav(b)] +class VerifierTests(unittest.TestCase): + def run_verifier(self,task,steps,answer,mutate=None,task_id=None): + with tempfile.TemporaryDirectory(prefix=f'osu-v-{task}-') as d: + root=Path(d);initial=root/'initial.db';after=root/'after.db';run=root/'run';run.mkdir();shutil.copy2(SEED,initial);shutil.copy2(SEED,after) + if mutate: + c=sqlite3.connect(after) + try:mutate(c);c.commit() + finally:c.close() + traj={'task_id':task_id or f'Ohio State University--{task}','start_url':url('/'),'steps':steps,'final_url':steps[-1].get('url_after',steps[-1].get('url')) if steps else url('/'),'final_answer':answer};(run/'trajectory.json').write_text(json.dumps(traj)) + r=subprocess.run([sys.executable,str(VERIFY_DIR/f'verify_{task}.py'),'--run_dir',str(run),'--initial_db',str(initial),'--after_db',str(after),'--no_llm','true'],capture_output=True,text=True,timeout=30) + try:v=json.loads(r.stdout) + except Exception as e:self.fail(f'task {task}: {r.stdout!r} {r.stderr!r} {e}') + return r.returncode,v + def positive(self,i): + F='/athletics/ohio-state-buckeyes-football';W='/athletics/ohio-state-buckeyes-wrestling';B='/athletics/ohio-state-buckeyes-mens-basketball';E='/athletics/ohio-state-buckeyes-fencing' + cases={ + 0:([nav('/')]+trans('/','/academics'),'Fisher College of Business dean is Anil Makhija.'), + 1:([nav('/about')],'Varsity Sports: 36.'), + 2:([nav('/athletics')]+trans('/athletics',F)+[nav('/athletics')]+trans('/athletics',W),'Football and wrestling both list the Big Ten.'), + 3:([nav('/athletics')]+trans('/athletics',F),'Head coach Ryan Day; recent record 11-2.'), + 4:([nav('/search?q=research+expenditures')]+trans('/search?q=research+expenditures','/news/ohio-state-sets-record-for-research-expenditures-at-13-billion'),'$1.3 billion; September 23, 2024.'), + 5:([nav('/about')],'Founded in 1870 as Ohio Agricultural and Mechanical College.'), + 6:([nav('/research')]+trans('/research','/research/translational-data-analytics-institute'),'Director Beth Plale; Data analytics, Machine learning, Health informatics, Social science.'), + 7:([nav('/about')],'Undergraduate: 46,820; graduate students: 14,000; difference: 32,820.'), + 8:([nav('/academics')],'Engineering: 8,000; Fisher: 4,500; Engineering has more, by 3,500.'), + 9:([nav('/programs?college=engineering')],'There are 3 distinct types: BS, MS, and PhD.'), + 10:([nav('/athletics')]+trans('/athletics',W),'Head coach Tom Ryan; home venue Covelli Center.'), + 11:([nav('/research')]+trans('/research','/research/ohio-supercomputer-center'),'Director David Bickel; founded 1987.'), + 12:([nav('/programs?q=Juris+Doctor')]+trans('/programs?q=Juris+Doctor','/programs/juris-doctor-jd'),'JD; 90 credits; 3 years.'), + 13:([nav('/departments')]+trans('/departments','/departments/department-of-mathematics'),'Chair James Cogdell; location 100 Mathematics Building.'), + 14:([nav('/athletics')]+trans('/athletics',F)+[nav('/athletics')]+trans('/athletics',B),'Football: Ohio Stadium; basketball: Value City Arena.'), + 15:([nav('/athletics')]+trans('/athletics',W)+[nav('/athletics')]+trans('/athletics',E),'Wrestling: 8; fencing: 2; wrestling has more by 6.'), + 16:([nav('/programs?degree=MBA')]+trans('/programs?degree=MBA','/programs/master-of-business-administration-mba'),'Deadline April 1; 60 credits; GRE not required.'), + 17:([nav('/search?q=cancer+research')]+trans('/search?q=cancer+research','/news/ohio-state-researchers-develop-breakthrough-cancer-immunotherapy'),'Ohio State Researchers Develop Breakthrough Cancer Immunotherapy by Jody Sheridan.'), + 18:([nav('/research')]+trans('/research','/research/james-cancer-hospital-and-solove-research-institute'),'Director William Farrar; Cancer research, Oncology, Clinical trials, Precision medicine.'), + 19:([nav('/research')]+trans('/research','/research/center-for-clean-hydrogen'),'Director Yann Guezennec; founded 2022; Hydrogen energy, Fuel cells, Green hydrogen, Energy storage.'), + };return cases[i] + def test_all_positive(self): + for i in range(20): + with self.subTest(i=i): + s,a=self.positive(i);rc,v=self.run_verifier(i,s,a);self.assertEqual(rc,0,v) + def test_answer_only_fails(self): + for i in range(20): + with self.subTest(i=i): + _,a=self.positive(i);rc,v=self.run_verifier(i,[],a);self.assertNotEqual(rc,0);self.assertFalse(v['pass']) + def test_wrong_task_id_fails(self): + for i in range(20): + with self.subTest(i=i): + s,a=self.positive(i);rc,v=self.run_verifier(i,s,a,task_id='Ohio State University--999');self.assertNotEqual(rc,0);self.assertEqual(v['reason'],'task_id_matches') + def test_external_origin_fails(self): + rc,v=self.run_verifier(1,[{'url':'https://evil.invalid/about','action':'navigate','params':{}}],'Varsity Sports: 36.');self.assertNotEqual(rc,0);self.assertFalse(v['pass']) + def test_database_mutation_fails_all(self): + def mutate(c):c.execute("UPDATE news_articles SET view_count=view_count+1 WHERE id=1") + for i in range(20): + with self.subTest(i=i): + s,a=self.positive(i);rc,v=self.run_verifier(i,s,a,mutate);self.assertNotEqual(rc,0);self.assertFalse(v['pass']) + def test_negated_answers_fail(self): + for i,a in {1:'There are not 36 varsity sports.',3:'Ryan Day is not coach; record 11-2.',5:'It was not founded in 1870 as Ohio Agricultural and Mechanical College.',16:'Deadline is not April 1; 60 credits; GRE not required.'}.items(): + s,_=self.positive(i);rc,v=self.run_verifier(i,s,a);self.assertNotEqual(rc,0);self.assertFalse(v['pass']) + def test_swapped_comparisons_fail(self): + for i,a in {7:'Undergraduate: 14,000; graduate: 46,820; difference 32,820.',8:'Engineering: 4,500; Fisher: 8,000; Engineering has more by 3,500.',14:'Football: Value City Arena; basketball: Ohio Stadium.',15:'Wrestling: 2; fencing: 8; wrestling has more by 6.'}.items(): + s,_=self.positive(i);rc,v=self.run_verifier(i,s,a);self.assertNotEqual(rc,0);self.assertFalse(v['pass']) + def test_required_filters_fail_when_missing(self): + for i in (9,12,16): + s,a=self.positive(i);s=[x for x in s if '?' not in x.get('url','') and '?' not in x.get('url_after','')];rc,v=self.run_verifier(i,s,a);self.assertNotEqual(rc,0);self.assertFalse(v['pass']) +if __name__=='__main__':unittest.main() diff --git a/sites/osu/verify/verify_0.py b/sites/osu/verify/verify_0.py new file mode 100644 index 00000000..ac3d0439 --- /dev/null +++ b/sites/osu/verify/verify_0.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_path +TASK_ID='Ohio State University--0' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('opened_academics',visited_path(t,'/academics'),'required=/academics');j.check('used_academics_link',clicked_transition(t,'/','/academics'),'home to academics click');j.check('answer_fisher_dean',contains_all(answer,('Fisher College of Business','Anil Makhija')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_1.py b/sites/osu/verify/verify_1.py new file mode 100644 index 00000000..96be9a74 --- /dev/null +++ b/sites/osu/verify/verify_1.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, contains_all, final_answer, has_number, load_run, parse_args, visited_path +TASK_ID='Ohio State University--1' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('opened_about',visited_path(t,'/about'),'required=/about');j.check('answer_varsity_sports',has_number(answer,36) and contains_all(answer,('varsity','sports')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_10.py b/sites/osu/verify/verify_10.py new file mode 100644 index 00000000..6e9814b2 --- /dev/null +++ b/sites/osu/verify/verify_10.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--10';PATH='/athletics/ohio-state-buckeyes-wrestling' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_wrestling_navigation',visited_in_order(t,[('/athletics',{}),(PATH,{})]) and clicked_transition(t,'/athletics',PATH),'athletics to wrestling');j.check('answer_coach_and_venue',contains_all(answer,('Tom Ryan','Covelli Center')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_11.py b/sites/osu/verify/verify_11.py new file mode 100644 index 00000000..443fd14f --- /dev/null +++ b/sites/osu/verify/verify_11.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, has_number, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--11';PATH='/research/ohio-supercomputer-center' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_osc_navigation',visited_in_order(t,[('/research',{}),(PATH,{})]) and clicked_transition(t,'/research',PATH),'research to OSC');j.check('answer_director_and_year',contains_all(answer,('David Bickel',)) and has_number(answer,1987),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_12.py b/sites/osu/verify/verify_12.py new file mode 100644 index 00000000..779f691f --- /dev/null +++ b/sites/osu/verify/verify_12.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, contains_word, final_answer, has_number, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--12';PATH='/programs/juris-doctor-jd' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_jd_search',visited_in_order(t,[('/programs',{'q':'Juris Doctor'}),(PATH,{})]) and clicked_transition(t,'/programs',PATH),'program search to JD');j.check('answer_jd_details',contains_word(answer,'JD') and has_number(answer,90) and has_number(answer,3) and contains_all(answer,('credits','years')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_13.py b/sites/osu/verify/verify_13.py new file mode 100644 index 00000000..184bfdcd --- /dev/null +++ b/sites/osu/verify/verify_13.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--13';PATH='/departments/department-of-mathematics' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_math_navigation',visited_in_order(t,[('/departments',{}),(PATH,{})]) and clicked_transition(t,'/departments',PATH),'departments to Mathematics');j.check('answer_chair_and_location',contains_all(answer,('James Cogdell','100 Mathematics Building')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_14.py b/sites/osu/verify/verify_14.py new file mode 100644 index 00000000..087634ae --- /dev/null +++ b/sites/osu/verify/verify_14.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, final_answer, load_run, parse_args, text_bound_in_comparison, visited_in_order +TASK_ID='Ohio State University--14';F='/athletics/ohio-state-buckeyes-football';B='/athletics/ohio-state-buckeyes-mens-basketball' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('athletics_then_both_details',visited_in_order(t,[('/athletics',{}),(F,{})]) and visited_in_order(t,[('/athletics',{}),(B,{})]),'listing before details');j.check('clicked_both_teams',clicked_transition(t,'/athletics',F) and clicked_transition(t,'/athletics',B),'visible team links used');j.check('football_venue_bound',text_bound_in_comparison(answer,'Ohio Stadium',('football',)),repr(answer));j.check('basketball_venue_bound',text_bound_in_comparison(answer,'Value City Arena',('basketball',)),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_15.py b/sites/osu/verify/verify_15.py new file mode 100644 index 00000000..9b63d051 --- /dev/null +++ b/sites/osu/verify/verify_15.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, affirmative_contains, check_common, check_read_only, clicked_transition, contains_any, final_answer, has_number, load_run, number_bound_in_comparison, parse_args, visited_in_order +TASK_ID='Ohio State University--15';W='/athletics/ohio-state-buckeyes-wrestling';F='/athletics/ohio-state-buckeyes-fencing' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('athletics_then_both_details',visited_in_order(t,[('/athletics',{}),(W,{})]) and visited_in_order(t,[('/athletics',{}),(F,{})]),'listing before details');j.check('clicked_both_teams',clicked_transition(t,'/athletics',W) and clicked_transition(t,'/athletics',F),'visible team links used');j.check('wrestling_titles_bound',number_bound_in_comparison(answer,8,('wrestling',)),repr(answer));j.check('fencing_titles_bound',number_bound_in_comparison(answer,2,('fencing',)),repr(answer));j.check('winner_and_difference',affirmative_contains(answer,'wrestling') and contains_any(answer,('more','higher')) and has_number(answer,6),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_16.py b/sites/osu/verify/verify_16.py new file mode 100644 index 00000000..3be4af80 --- /dev/null +++ b/sites/osu/verify/verify_16.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, affirmative_contains, check_common, check_read_only, clicked_transition, contains_all, contains_word, final_answer, load_run, parse_args, visited_in_order, visited_query +TASK_ID='Ohio State University--16';PATH='/programs/master-of-business-administration-mba' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('mba_filter',visited_query(t,'/programs',{'degree':'MBA'}),'degree=MBA');j.check('ordered_mba_navigation',visited_in_order(t,[('/programs',{'degree':'MBA'}),(PATH,{})]) and clicked_transition(t,'/programs',PATH),'filtered programs to MBA');j.check('answer_mba_details',contains_all(answer,('April 1','credits')) and contains_word(answer,'60') and contains_word(answer,'GRE') and affirmative_contains(answer,'not required'),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_17.py b/sites/osu/verify/verify_17.py new file mode 100644 index 00000000..9182b790 --- /dev/null +++ b/sites/osu/verify/verify_17.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--17';PATH='/news/ohio-state-researchers-develop-breakthrough-cancer-immunotherapy';TITLE='Ohio State Researchers Develop Breakthrough Cancer Immunotherapy' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_cancer_search',visited_in_order(t,[('/search',{'q':'cancer research'}),(PATH,{})]) and clicked_transition(t,'/search',PATH),'search to exact article');j.check('answer_title_author',contains_all(answer,(TITLE,'Jody Sheridan')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_18.py b/sites/osu/verify/verify_18.py new file mode 100644 index 00000000..a1de44c7 --- /dev/null +++ b/sites/osu/verify/verify_18.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--18';PATH='/research/james-cancer-hospital-and-solove-research-institute' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_james_navigation',visited_in_order(t,[('/research',{}),(PATH,{})]) and clicked_transition(t,'/research',PATH),'research to James');j.check('answer_director_focus',contains_all(answer,('William Farrar','Cancer research','Oncology','Clinical trials','Precision medicine')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_19.py b/sites/osu/verify/verify_19.py new file mode 100644 index 00000000..9eb2c378 --- /dev/null +++ b/sites/osu/verify/verify_19.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, has_number, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--19';PATH='/research/center-for-clean-hydrogen' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_hydrogen_navigation',visited_in_order(t,[('/research',{}),(PATH,{})]) and clicked_transition(t,'/research',PATH),'research to Clean Hydrogen');j.check('answer_director_year_focus',contains_all(answer,('Yann Guezennec','Hydrogen energy','Fuel cells','Green hydrogen','Energy storage')) and has_number(answer,2022),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_2.py b/sites/osu/verify/verify_2.py new file mode 100644 index 00000000..4e382a76 --- /dev/null +++ b/sites/osu/verify/verify_2.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--2';F='/athletics/ohio-state-buckeyes-football';W='/athletics/ohio-state-buckeyes-wrestling' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('athletics_then_both_details',visited_in_order(t,[('/athletics',{}),(F,{})]) and visited_in_order(t,[('/athletics',{}),(W,{})]),'listing precedes details');j.check('clicked_both_teams',clicked_transition(t,'/athletics',F) and clicked_transition(t,'/athletics',W),'visible team links used');j.check('answer_big_ten_both',contains_all(answer,('football','wrestling','Big Ten')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_3.py b/sites/osu/verify/verify_3.py new file mode 100644 index 00000000..5672b752 --- /dev/null +++ b/sites/osu/verify/verify_3.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--3';PATH='/athletics/ohio-state-buckeyes-football' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_football_navigation',visited_in_order(t,[('/athletics',{}),(PATH,{})]) and clicked_transition(t,'/athletics',PATH),'athletics listing to football');j.check('answer_coach_and_record',contains_all(answer,('Ryan Day','11-2')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_4.py b/sites/osu/verify/verify_4.py new file mode 100644 index 00000000..a379f85d --- /dev/null +++ b/sites/osu/verify/verify_4.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, has_number, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--4';PATH='/news/ohio-state-sets-record-for-research-expenditures-at-13-billion' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_news_search',visited_in_order(t,[('/search',{'q':'research expenditures'}),(PATH,{})]) and clicked_transition(t,'/search',PATH),'search to exact article');j.check('answer_amount_date',has_number(answer,1.3) and contains_all(answer,('billion','September','23','2024')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_5.py b/sites/osu/verify/verify_5.py new file mode 100644 index 00000000..8a03efc7 --- /dev/null +++ b/sites/osu/verify/verify_5.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, contains_all, final_answer, has_number, load_run, parse_args, visited_path +TASK_ID='Ohio State University--5' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('opened_about',visited_path(t,'/about'),'required=/about');j.check('answer_founding',has_number(answer,1870) and contains_all(answer,('Ohio Agricultural and Mechanical College',)),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_6.py b/sites/osu/verify/verify_6.py new file mode 100644 index 00000000..029ee6f4 --- /dev/null +++ b/sites/osu/verify/verify_6.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, clicked_transition, contains_all, final_answer, load_run, parse_args, visited_in_order +TASK_ID='Ohio State University--6';PATH='/research/translational-data-analytics-institute' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('ordered_tdai_navigation',visited_in_order(t,[('/research',{}),(PATH,{})]) and clicked_transition(t,'/research',PATH),'research listing to TDAI');j.check('answer_director_and_focus',contains_all(answer,('Beth Plale','Data analytics','Machine learning','Health informatics','Social science')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_7.py b/sites/osu/verify/verify_7.py new file mode 100644 index 00000000..869f4916 --- /dev/null +++ b/sites/osu/verify/verify_7.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, final_answer, has_number, load_run, number_bound_in_comparison, parse_args, visited_path +TASK_ID='Ohio State University--7' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('opened_about',visited_path(t,'/about'),'required=/about');j.check('undergraduate_bound',number_bound_in_comparison(answer,46820,('undergraduate','undergrads')),repr(answer));j.check('graduate_bound',number_bound_in_comparison(answer,14000,('graduate','graduate students')),repr(answer));j.check('exact_difference',has_number(answer,32820),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_8.py b/sites/osu/verify/verify_8.py new file mode 100644 index 00000000..4f4bbb90 --- /dev/null +++ b/sites/osu/verify/verify_8.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, affirmative_contains, check_common, check_read_only, contains_any, final_answer, has_number, load_run, number_bound_in_comparison, parse_args, visited_path +TASK_ID='Ohio State University--8' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('opened_academics',visited_path(t,'/academics'),'required=/academics');j.check('engineering_count_bound',number_bound_in_comparison(answer,8000,('Engineering',)),repr(answer));j.check('fisher_count_bound',number_bound_in_comparison(answer,4500,('Fisher',)),repr(answer));j.check('difference_and_winner',has_number(answer,3500) and affirmative_contains(answer,'Engineering') and contains_any(answer,('more','higher')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_9.py b/sites/osu/verify/verify_9.py new file mode 100644 index 00000000..d732cbe4 --- /dev/null +++ b/sites/osu/verify/verify_9.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from verify_lib import Judge, check_common, check_read_only, contains_word, final_answer, has_number, load_run, parse_args, visited_query +TASK_ID='Ohio State University--9' +def main(): + a=parse_args();t=load_run(a.run_dir);j=Judge(TASK_ID);answer=final_answer(t);check_common(j,t,TASK_ID);j.check('engineering_filter',visited_query(t,'/programs',{'college':'engineering'}),'college=engineering');j.check('answer_all_types',all(contains_word(answer,value) for value in ('BS','MS','PhD')) and has_number(answer,3),repr(answer));j.check('no_extra_degree_types',not any(contains_word(answer,value) for value in ('BA','MA','MBA','JD','MD','MPH','PharmD','DVM','OD')),repr(answer));check_read_only(j,a);j.emit() +if __name__=='__main__':main() diff --git a/sites/osu/verify/verify_lib.py b/sites/osu/verify/verify_lib.py new file mode 100644 index 00000000..56ccc187 --- /dev/null +++ b/sites/osu/verify/verify_lib.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +"""Shared deterministic helpers for Ohio State University task verifiers.""" + +from __future__ import annotations + +import argparse +import ipaddress +import json +import os +import re +import sqlite3 +import subprocess +import tempfile +import unicodedata +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable, Sequence +from urllib.parse import parse_qs, urlparse + +SITE = "osu" +DEFAULT_CONTAINER = os.environ.get("WH_CONTAINER", "wh-review") + +@dataclass(frozen=True) +class VerifyArgs: + run_dir: str + initial_db: str | None + after_db: str | None + container: str + no_llm: bool + + +def _bool_value(value: str) -> bool: + return str(value).casefold() in {"1", "true", "yes", "on"} + + +def parse_args() -> VerifyArgs: + parser = argparse.ArgumentParser() + parser.add_argument("--run_dir", required=True) + parser.add_argument("--initial_db") + parser.add_argument("--after_db") + parser.add_argument("--container", default=DEFAULT_CONTAINER) + parser.add_argument("--no_llm", nargs="?", const=True, default=False, type=_bool_value) + args = parser.parse_args() + run_dir = Path(args.run_dir) + initial_snapshot = run_dir / "initial.db" + after_snapshot = run_dir / "after.db" + return VerifyArgs( + run_dir=args.run_dir, + initial_db=args.initial_db or (str(initial_snapshot) if initial_snapshot.is_file() else None), + after_db=args.after_db or (str(after_snapshot) if after_snapshot.is_file() else None), + container=args.container, + no_llm=bool(args.no_llm), + ) + + +def load_run(run_dir: str | os.PathLike[str]) -> dict[str, Any]: + trajectory = json.loads((Path(run_dir) / "trajectory.json").read_text(encoding="utf-8")) + if not isinstance(trajectory, dict): + raise ValueError("trajectory.json must contain a JSON object") + return trajectory + + +def normalize_text(value: Any) -> str: + text = unicodedata.normalize("NFKC", str(value or "")) + text = text.replace("’", "'").replace("“", '"').replace("”", '"').replace("–", "-").replace("—", "-") + return re.sub(r"\s+", " ", text).strip().casefold() + + +def final_answer(trajectory: dict[str, Any]) -> str: + return str(trajectory.get("final_answer") or "").strip() + + +def trajectory_urls(trajectory: dict[str, Any]) -> list[str]: + urls: list[str] = [] + if trajectory.get("start_url"): + urls.append(str(trajectory["start_url"])) + for step in trajectory.get("steps") or []: + if not isinstance(step, dict): + continue + for key in ("url_before", "url", "url_after"): + value = str(step.get(key) or "") + if value and (not urls or value != urls[-1]): + urls.append(value) + final_url = str(trajectory.get("final_url") or "") + if final_url and (not urls or final_url != urls[-1]): + urls.append(final_url) + return urls + + +def _loopback(hostname: str) -> bool: + if hostname.casefold() == "localhost": + return True + try: + return ipaddress.ip_address(hostname).is_loopback + except ValueError: + return False + + +def is_site_url(url: str, trajectory: dict[str, Any]) -> bool: + parsed = urlparse(str(url or "")) + start = urlparse(str(trajectory.get("start_url") or "")) + return bool( + parsed.scheme in {"http", "https"} + and parsed.hostname + and start.hostname + and _loopback(parsed.hostname) + and _loopback(start.hostname) + and parsed.scheme == start.scheme + and parsed.port == start.port + ) + + +def normalized_path(url: str) -> str: + path = urlparse(str(url or "")).path or "/" + return path.rstrip("/") or "/" + + +def query_matches(url: str, expected: dict[str, str]) -> bool: + params = parse_qs(urlparse(url).query) + return all(normalize_text((params.get(key) or [""])[0]) == normalize_text(value) for key, value in expected.items()) + + +def visited_path(trajectory: dict[str, Any], path: str) -> bool: + expected = normalized_path(path) + return any(is_site_url(url, trajectory) and normalized_path(url) == expected for url in trajectory_urls(trajectory)) + + +def visited_query(trajectory: dict[str, Any], path: str, expected: dict[str, str]) -> bool: + return any( + is_site_url(url, trajectory) + and normalized_path(url) == normalized_path(path) + and query_matches(url, expected) + for url in trajectory_urls(trajectory) + ) + + +def visited_in_order(trajectory: dict[str, Any], requirements: list[tuple[str, dict[str, str]]]) -> bool: + urls = trajectory_urls(trajectory) + cursor = 0 + for path, query in requirements: + found = False + for index in range(cursor, len(urls)): + url = urls[index] + if is_site_url(url, trajectory) and normalized_path(url) == normalized_path(path) and query_matches(url, query): + cursor = index + 1 + found = True + break + if not found: + return False + return True + + +def transition_pairs(trajectory: dict[str, Any]): + steps = trajectory.get("steps") or [] + for index, step in enumerate(steps): + if not isinstance(step, dict): + continue + current = str(step.get("url") or step.get("url_before") or "") + if not is_site_url(current, trajectory): + continue + following = str(step.get("url_after") or "") + if not following and index + 1 < len(steps) and isinstance(steps[index + 1], dict): + following = str(steps[index + 1].get("url") or steps[index + 1].get("url_after") or "") + if following and is_site_url(following, trajectory): + yield normalize_text(step.get("action")), current, following + + +def clicked_transition(trajectory: dict[str, Any], from_path: str, to_path: str) -> bool: + return any( + action == "click" + and normalized_path(current) == normalized_path(from_path) + and normalized_path(following) == normalized_path(to_path) + for action, current, following in transition_pairs(trajectory) + ) + + +def submitted_from_path(trajectory: dict[str, Any], path: str, destination: str | None = None) -> bool: + for action, current, following in transition_pairs(trajectory): + if action != "click" or normalized_path(current) != normalized_path(path): + continue + if destination is None or normalized_path(following) == normalized_path(destination): + return True + return False + + +def input_values(trajectory: dict[str, Any], path: str | None = None) -> list[str]: + values: list[str] = [] + for step in trajectory.get("steps") or []: + if not isinstance(step, dict) or normalize_text(step.get("action")) not in {"input", "fill", "type", "select"}: + continue + url = str(step.get("url") or step.get("url_before") or "") + if not is_site_url(url, trajectory) or (path and normalized_path(url) != normalized_path(path)): + continue + params = step.get("params") or {} + value = params.get("text", params.get("value", params.get("option", params.get("label")))) if isinstance(params, dict) else None + if value is not None: + values.append(str(value)) + return values + + +def entered_text(trajectory: dict[str, Any], expected: str, path: str | None = None) -> bool: + expected_value = normalize_text(expected) + return any(normalize_text(value) == expected_value for value in input_values(trajectory, path)) + + +def last_entered_email(trajectory: dict[str, Any], path: str = "/login") -> str: + emails = [normalize_text(value) for value in input_values(trajectory, path) if re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", value.strip())] + return emails[-1] if emails else "" + + +def login_submitted_as(trajectory: dict[str, Any], email: str) -> bool: + return visited_path(trajectory, "/login") and last_entered_email(trajectory) == normalize_text(email) and submitted_from_path(trajectory, "/login") + + +NEGATIONS = {"not", "no", "never", "without", "isn't", "isnt", "wasn't", "wasnt", "doesn't", "doesnt", "didn't", "didnt"} + + +def _negated_before(text: str, start: int) -> bool: + clause = re.split(r"[.!?;:\n]+|\b(?:and|but|however|instead)\b", text[:start])[-1] + words = re.findall(r"[a-z0-9]+(?:'[a-z]+)?", clause) + return any(word in NEGATIONS for word in words) + + +def _negated_after(text: str, end: int) -> bool: + suffix = re.sub(r"^\s*[-,:;!?]*\s*", "", text[end:]) + return re.match(r"(?:(?:is|was|does|did|are|were)\s+)?(?:not|never|no)\b|(?:isn't|isnt|wasn't|wasnt|doesn't|doesnt|didn't|didnt|aren't|arent|weren't|werent)\b", suffix) is not None + + +def affirmative_contains(text: Any, expected: Any) -> bool: + normalized = normalize_text(text) + needle = normalize_text(expected) + matches = list(re.finditer(re.escape(needle), normalized)) + if not needle or not matches: + return False + match = matches[-1] + return not _negated_before(normalized, match.start()) and not _negated_after(normalized, match.end()) + + +def contains_all(text: Any, expected: Iterable[Any]) -> bool: + return all(affirmative_contains(text, value) for value in expected) + + +def contains_any(text: Any, expected: Iterable[Any]) -> bool: + return any(affirmative_contains(text, value) for value in expected) + + +def contains_word(text: Any, expected: str) -> bool: + normalized = normalize_text(text) + return re.search(rf"(? list[re.Match[str]]: + normalized = normalize_text(text) + matches = [] + for match in re.finditer(r"(? bool: + return bool(number_matches(text, value)) + + +def number_bound_to(text: Any, value: int | float, labels: Sequence[str], distance: int = 140) -> bool: + normalized = normalize_text(text) + for match in number_matches(normalized, value): + window = normalized[max(0, match.start() - distance):min(len(normalized), match.end() + distance)] + if any(normalize_text(label) in window for label in labels): + return True + return False + + +def number_bound_in_comparison(text: Any, value: int | float, labels: Sequence[str]) -> bool: + normalized = normalize_text(text) + segments = re.split(r"\b(?:versus|vs\.?|while|compared (?:with|to))\b|[;\n]", normalized) + return any(has_number(segment, value) and any(normalize_text(label) in segment for label in labels) for segment in segments) + + +def text_bound_in_comparison(text: Any, value: str, labels: Sequence[str]) -> bool: + normalized = normalize_text(text) + segments = re.split(r"\b(?:versus|vs\.?|while|compared (?:with|to))\b|[;\n]", normalized) + return any(normalize_text(value) in segment and any(normalize_text(label) in segment for label in labels) for segment in segments) + + +def fetch_db(container: str, kind: str) -> str: + if kind not in {"instance", "instance_seed"}: + raise ValueError(f"unsupported database kind: {kind}") + handle, destination = tempfile.mkstemp(prefix=f"osu_{kind}_", suffix=".db") + os.close(handle) + source = f"{container}:/opt/WebSyn/{SITE}/{kind}/{SITE}.db" + result = subprocess.run(["docker", "cp", source, destination], capture_output=True, text=True, check=False) + if result.returncode: + Path(destination).unlink(missing_ok=True) + raise RuntimeError(result.stderr.strip() or f"could not copy {source}") + return destination + + +def resolve_db(explicit: str | None, container: str, kind: str) -> str | None: + if explicit: + return explicit if Path(explicit).is_file() else None + try: + return fetch_db(container, kind) + except (OSError, RuntimeError): + return None + + +def db_query(path: str, sql: str, params: Sequence[Any] = ()) -> list[sqlite3.Row]: + connection = sqlite3.connect(path) + connection.row_factory = sqlite3.Row + try: + return connection.execute(sql, params).fetchall() + finally: + connection.close() + + +def row_dicts(path: str, sql: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + return [dict(row) for row in db_query(path, sql, params)] + + +def database_tables(path: str) -> list[str]: + return [str(row["name"]) for row in db_query(path, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")] + + +def table_snapshot(path: str, table: str) -> list[tuple[Any, ...]]: + return [tuple(row) for row in db_query(path, f'SELECT * FROM "{table}" ORDER BY rowid')] + + +def changed_tables(initial_db: str, after_db: str) -> set[str]: + initial_tables = database_tables(initial_db) + if initial_tables != database_tables(after_db): + return {""} + return {table for table in initial_tables if table_snapshot(initial_db, table) != table_snapshot(after_db, table)} + + +def database_unchanged(initial_db: str | None, after_db: str | None) -> bool: + return bool(initial_db and after_db and not changed_tables(initial_db, after_db)) + + +def check_common(judge: "Judge", trajectory: dict[str, Any], task_id: str) -> None: + judge.check("task_id_matches", str(trajectory.get("task_id") or "") == task_id, f"observed={trajectory.get('task_id')!r}") + judge.check("final_answer_nonempty", bool(final_answer(trajectory)), repr(final_answer(trajectory))) + judge.check("start_url_is_site", is_site_url(str(trajectory.get("start_url") or ""), trajectory), f"start_url={trajectory.get('start_url')!r}") + + +def check_read_only(judge: "Judge", args: VerifyArgs) -> tuple[str | None, str | None]: + initial = resolve_db(args.initial_db, args.container, "instance_seed") + after = resolve_db(args.after_db, args.container, "instance") + judge.check("databases_readable", bool(initial and after), f"initial={initial} after={after}") + judge.check("read_only_database_unchanged", database_unchanged(initial, after), "complete database comparison") + return initial, after + + +class Judge: + def __init__(self, task_id: str, no_llm: bool = False): + self.task_id = task_id + self.passed = True + self.reason = "" + self.evidence: list[str] = [] + + def check(self, name: str, condition: bool, evidence: str = "", llm: bool = False) -> bool: + self.evidence.append(f"[{'PASS' if condition else 'FAIL'}] {name}: {evidence}") + if not condition: + self.passed = False + if not self.reason: + self.reason = name + return bool(condition) + + def emit(self) -> None: + print(json.dumps({"task_id": self.task_id, "pass": self.passed, "reason": self.reason, "evidence": self.evidence}, ensure_ascii=False, indent=2)) + raise SystemExit(0 if self.passed else 1) diff --git a/websyn_start.sh b/websyn_start.sh index 4b29a5c9..4c03c334 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -1,11 +1,11 @@ #!/bin/bash -# WebSyn startup: launch all mirror sites, then exec the original CMD. +# WebSyn startup: launch all mirror sites, then exec the control plane. # This preserves the base image's browser env server (port 8100) as PID 1. set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted) + cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR" @@ -80,6 +80,6 @@ done echo "[WebSyn] Starting control server on :8101 (PID 1)..." # Control server becomes PID 1 — receives SIGTERM on `docker stop`, -# keeps the container alive as long as it's running. The site +# keeps the container alive as long as it is running. Site # subprocesses are managed via /tmp/websyn_pids/.pid. exec python3 /opt/control_server.py --port 8101