From ea4d5eb2859d28cb49f2a63dba8168f8a797c937 Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 01:25:13 +0530 Subject: [PATCH 01/14] Local Development Sandbox, setup made easier --- .devcontainer/devcontainer.json | 51 ++++ .github/workflows/onboarding-smoke-test.yml | 101 +++++++ .gitignore | 3 + alphaonelabs-education-website@1.0.0 | 0 bash | 5 + docker-compose.dev.yml | 95 ++++++ package.json | 12 + poetry.toml | 1 + scripts/dev.sh | 107 +++++++ scripts/doctor.sh | 228 +++++++++++++++ scripts/setup.sh | 301 ++++++++++++++++++++ web/views.py | 11 +- 12 files changed, 905 insertions(+), 10 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/onboarding-smoke-test.yml create mode 100644 alphaonelabs-education-website@1.0.0 create mode 100644 bash create mode 100644 docker-compose.dev.yml create mode 100644 package.json create mode 100755 scripts/dev.sh create mode 100755 scripts/doctor.sh create mode 100755 scripts/setup.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..99da62351 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,51 @@ +// ============================================================================= +// VS Code Dev Container configuration +// +// Opens this project inside a Docker container with all services running. +// Uses docker-compose.dev.yml for MySQL + Redis alongside the web container. +// +// Prerequisites: Docker Desktop + VS Code "Dev Containers" extension +// ============================================================================= +{ + "name": "Alpha One Labs — Dev", + "dockerComposeFile": "../docker-compose.dev.yml", + "service": "web", + "workspaceFolder": "/app", + // Forward the Django dev server port to the host + "forwardPorts": [ + 8000, + 3306, + 6379 + ], + // Run after the container is created (first time only) + "postCreateCommand": "pip install poetry==1.8.3 && poetry config virtualenvs.create false --local && poetry install --no-interaction", + // Run every time the container starts + "postStartCommand": "python manage.py migrate --no-input && python manage.py collectstatic --noinput", + // VS Code settings inside the container + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } + }, + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "ms-python.isort", + "ms-python.flake8", + "batisteo.vscode-django", + "monosans.djlint" + ] + } + }, + // Features to install in the container + "features": { + "ghcr.io/devcontainers/features/git:1": {} + } +} \ No newline at end of file diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml new file mode 100644 index 000000000..c5eccaaff --- /dev/null +++ b/.github/workflows/onboarding-smoke-test.yml @@ -0,0 +1,101 @@ +# ============================================================================= +# Onboarding Smoke Test +# +# Ensures that the setup script and dev server work on every PR. +# Uses SQLite (no Docker/MySQL needed) so the CI job is fast and simple. +# +# This test guards against regressions that would break new-contributor +# onboarding — if this job fails, someone can't get started. +# ============================================================================= +name: Onboarding Smoke Test + +on: + push: + branches: [main] + paths: + - 'scripts/**' + - 'package.json' + - 'pyproject.toml' + - 'poetry.lock' + - '.github/workflows/onboarding-smoke-test.yml' + pull_request: + paths: + - 'scripts/**' + - 'package.json' + - 'pyproject.toml' + - 'poetry.lock' + - '.github/workflows/onboarding-smoke-test.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + smoke-test: + name: Setup & Boot + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Set up Node.js (for npm run commands) + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Run setup script + run: | + chmod +x scripts/setup.sh + npm run setup + + - name: Run doctor + run: | + chmod +x scripts/doctor.sh + npm run doctor || true # Warnings are OK in CI (e.g. no Docker) + + - name: Boot server and hit health endpoint + run: | + chmod +x scripts/dev.sh + + # Start the dev server in the background + npm run dev & + SERVER_PID=$! + + # Wait for the server to be ready (up to 30 seconds) + echo "Waiting for server to start..." + for i in $(seq 1 30); do + if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/en/ | grep -qE "200|301|302"; then + echo "Server is up! (attempt ${i})" + break + fi + if [ $i -eq 30 ]; then + echo "Server failed to start within 30 seconds" + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + sleep 1 + done + + # Verify the response + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/en/) + echo "HTTP response code: ${HTTP_CODE}" + + if echo "${HTTP_CODE}" | grep -qE "200|301|302"; then + echo "✔ Health check passed!" + else + echo "✖ Health check failed with HTTP ${HTTP_CODE}" + kill $SERVER_PID 2>/dev/null || true + exit 1 + fi + + # Clean shutdown + kill $SERVER_PID 2>/dev/null || true + wait $SERVER_PID 2>/dev/null || true + echo "✔ Server stopped cleanly" diff --git a/.gitignore b/.gitignore index 62334fde2..20822e886 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ backup.json ansible/inventory.yml education-website-*.json *.sql +node_modules/ +.venv/ +poetry.toml diff --git a/alphaonelabs-education-website@1.0.0 b/alphaonelabs-education-website@1.0.0 new file mode 100644 index 000000000..e69de29bb diff --git a/bash b/bash new file mode 100644 index 000000000..58caabc4b --- /dev/null +++ b/bash @@ -0,0 +1,5 @@ + +[1/7] Checking dependencies... + ✔ Python 3.14 + ✔ pip available + ⚠ Poetry not found — installing Poetry 1.8.3... diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..eefed1956 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,95 @@ +# ============================================================================= +# docker-compose.dev.yml — Local development services +# +# Usage: docker compose -f docker-compose.dev.yml up +# +# Provides: Django (hot-reload), MySQL 8, Redis 7 +# Data persists across restarts via named volumes. +# ============================================================================= + +services: + # --------------------------------------------------------------------------- + # MySQL 8 — relational database + # --------------------------------------------------------------------------- + db: + image: mysql:8.0 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: education_website + MYSQL_USER: django + MYSQL_PASSWORD: django_password + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-prootpassword"] + interval: 10s + timeout: 5s + retries: 5 + + # --------------------------------------------------------------------------- + # Redis 7 — channel layer for Django Channels (WebSockets) + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + + # --------------------------------------------------------------------------- + # Django web application — development server with hot-reload + # --------------------------------------------------------------------------- + web: + build: + context: . + dockerfile: Dockerfile + command: > + bash -c " + echo 'Waiting for MySQL...' && + while ! mysqladmin ping -h db -u root -prootpassword --silent 2>/dev/null; do + sleep 1 + done && + echo 'MySQL is ready!' && + python manage.py migrate --no-input && + python manage.py create_test_data && + python manage.py collectstatic --noinput && + echo '========================================' && + echo ' Dev server: http://localhost:8000' && + echo '========================================' && + python manage.py runserver 0.0.0.0:8000 + " + volumes: + # Bind-mount the source code for hot-reload + - .:/app + # Prevent the container's venv from being overwritten by the bind mount + - /app/.venv + ports: + - "8000:8000" + environment: + - DATABASE_URL=mysql://root:rootpassword@db:3306/education_website + - REDIS_URL=redis://redis:6379/0 + - ENVIRONMENT=development + - DEBUG=True + - SECRET_KEY=docker-dev-secret-key-not-for-production + - DJANGO_SETTINGS_MODULE=web.settings + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + +volumes: + mysql_data: + driver: local + redis_data: + driver: local diff --git a/package.json b/package.json new file mode 100644 index 000000000..e8fa000d2 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "alphaonelabs-education-website", + "version": "1.0.0", + "private": true, + "description": "Script runner for Alpha One Labs Education Platform — no Node dependencies required.", + "scripts": { + "setup": "bash scripts/setup.sh", + "dev": "bash scripts/dev.sh", + "doctor": "bash scripts/doctor.sh", + "test": "bash -c 'python manage.py test'" + } +} diff --git a/poetry.toml b/poetry.toml index 084377a03..437b344df 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,2 +1,3 @@ [virtualenvs] create = false +in-project = true diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 000000000..2d986327b --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# ============================================================================= +# dev.sh — Start the local development server +# +# Usage: npm run dev OR bash scripts/dev.sh +# +# Starts Django's development server with hot-reload. Optionally checks Redis +# availability if Redis-backed channels are configured. Stops cleanly on Ctrl+C. +# ============================================================================= +set -euo pipefail + +# -- Colours & helpers -------------------------------------------------------- +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m' +CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +info() { echo -e "${BLUE}${BOLD}$1${NC}"; } +ok() { echo -e "${GREEN} ✔ $1${NC}"; } +warn() { echo -e "${YELLOW} ⚠ $1${NC}"; } +fail() { echo -e "${RED} ✖ $1${NC}"; } + +# -- Resolve project root ----------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +cd "${PROJECT_ROOT}" + +# -- Pre-flight checks -------------------------------------------------------- +info "Pre-flight checks..." + +# Ensure .env exists +if [ ! -f "${PROJECT_ROOT}/.env" ]; then + fail ".env file not found. Run 'npm run setup' first." + exit 1 +fi +ok ".env file found" + +# Detect Python from the Poetry venv or fall back to system python +if [ -f "${PROJECT_ROOT}/.venv/bin/python" ]; then + PYTHON="${PROJECT_ROOT}/.venv/bin/python" +elif command -v poetry &>/dev/null; then + PYTHON="$(poetry env info -e 2>/dev/null || echo python3)" +else + PYTHON="python3" +fi +ok "Python: ${PYTHON}" + +# Check if port 8000 is already in use +if command -v lsof &>/dev/null; then + if lsof -Pi :8000 -sTCP:LISTEN -t &>/dev/null; then + warn "Port 8000 is already in use — the server may fail to bind." + warn "Run 'npm run doctor' for help, or kill the process on port 8000." + fi +fi + +# Optional: Check Redis connectivity (non-blocking) +REDIS_URL=$(grep '^REDIS_URL=' "${PROJECT_ROOT}/.env" 2>/dev/null | cut -d= -f2- || echo "") +if [ -n "${REDIS_URL}" ] && [ "${REDIS_URL}" != "redis://127.0.0.1:6379/0" ]; then + # Custom Redis URL configured — warn if it's unreachable + if command -v redis-cli &>/dev/null; then + if ! redis-cli -u "${REDIS_URL}" ping &>/dev/null 2>&1; then + warn "Redis at ${REDIS_URL} is not reachable." + warn "WebSocket features (chat, whiteboard) won't work without Redis." + else + ok "Redis is reachable" + fi + fi +elif command -v redis-cli &>/dev/null; then + if redis-cli ping &>/dev/null 2>&1; then + ok "Redis is reachable (localhost)" + else + warn "Redis is not running locally. WebSocket features won't work." + warn "Start Redis with: redis-server OR docker run -d -p 6379:6379 redis:7-alpine" + fi +else + warn "Redis CLI not found — skipping Redis check." + warn "WebSocket features (chat, whiteboard) require Redis at runtime." +fi + +# -- Collect static files (quick, idempotent) --------------------------------- +info "Collecting static files..." +"${PYTHON}" manage.py collectstatic --noinput --verbosity=0 2>&1 || true +ok "Static files ready" + +# -- Trap Ctrl+C for clean shutdown ------------------------------------------- +cleanup() { + echo "" + echo -e "${YELLOW}Shutting down...${NC}" + # Kill all child processes in this process group + kill -- -$$ 2>/dev/null || true + exit 0 +} +trap cleanup INT TERM + +# -- Start the development server --------------------------------------------- +echo "" +echo -e "${GREEN}${BOLD}══════════════════════════════════════════════════════════${NC}" +echo -e "${GREEN}${BOLD} Alpha One Labs — Development Server${NC}" +echo -e "${GREEN}${BOLD}══════════════════════════════════════════════════════════${NC}" +echo "" +echo -e " ${CYAN}Local:${NC} http://localhost:8000" +echo -e " ${CYAN}Network:${NC} http://0.0.0.0:8000" +echo -e " ${CYAN}Admin:${NC} http://localhost:8000/a-dmin-url123/" +echo "" + +# Start Django dev server — filter out startup noise so the banner above +# stays as the last visible output. Request logs still pass through. +"${PYTHON}" manage.py runserver 0.0.0.0:8000 2>&1 \ + | grep --line-buffered -v -E "^(Watching for file changes|Performing system checks|System check identified|Django version|Starting development server|Quit the server with|$)" diff --git a/scripts/doctor.sh b/scripts/doctor.sh new file mode 100755 index 000000000..5a24c6e77 --- /dev/null +++ b/scripts/doctor.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# ============================================================================= +# doctor.sh — Diagnose common development environment problems +# +# Usage: npm run doctor OR bash scripts/doctor.sh +# +# Checks for common issues and prints human-readable fixes, not stack traces. +# ============================================================================= +set -uo pipefail + +# -- Colours & helpers -------------------------------------------------------- +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m' +BOLD='\033[1m'; NC='\033[0m' + +ok() { echo -e " ${GREEN}✔${NC} $1"; PASS=$((PASS + 1)); } +warn() { echo -e " ${YELLOW}⚠${NC} $1"; WARN=$((WARN + 1)); } +fail() { echo -e " ${RED}✖${NC} $1"; FAIL=$((FAIL + 1)); } +fix() { echo -e " ${BLUE}→ Fix:${NC} $1"; } + +PASS=0; WARN=0; FAIL=0 + +# -- Resolve project root ----------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +cd "${PROJECT_ROOT}" + +echo "" +echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ Alpha One Labs — Environment Doctor ║${NC}" +echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}" +echo "" + +# ============================================================================= +# 1. Python version +# ============================================================================= +echo -e "${BOLD}Python${NC}" + +if command -v python3 &>/dev/null; then + PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")') + PY_MAJOR=$(python3 -c 'import sys; print(sys.version_info.major)') + PY_MINOR=$(python3 -c 'import sys; print(sys.version_info.minor)') + if [ "${PY_MAJOR}" -ge 3 ] && [ "${PY_MINOR}" -ge 10 ]; then + ok "Python ${PY_VER} (≥ 3.10 required)" + else + fail "Python ${PY_VER} found — 3.10+ is required" + fix "Install Python 3.10+ from https://www.python.org/downloads/" + fi +else + fail "Python 3 is not installed" + fix "Install Python 3.10+ from https://www.python.org/downloads/" +fi + +# ============================================================================= +# 2. Poetry +# ============================================================================= +echo "" +echo -e "${BOLD}Package Manager${NC}" + +if command -v poetry &>/dev/null; then + POETRY_VER=$(poetry --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') + ok "Poetry ${POETRY_VER}" +else + fail "Poetry is not installed" + fix "pip install poetry==1.8.3" +fi + +# ============================================================================= +# 3. Virtual environment +# ============================================================================= +echo "" +echo -e "${BOLD}Virtual Environment${NC}" + +if [ -d "${PROJECT_ROOT}/.venv" ]; then + ok ".venv directory exists" + if [ -f "${PROJECT_ROOT}/.venv/bin/python" ]; then + VENV_PY=$("${PROJECT_ROOT}/.venv/bin/python" --version 2>&1) + ok "venv Python: ${VENV_PY}" + else + warn ".venv exists but python binary not found" + fix "Run: npm run setup" + fi +else + warn "No .venv directory — dependencies may not be installed" + fix "Run: npm run setup" +fi + +# macOS Gatekeeper quarantine check +if [[ "$OSTYPE" == "darwin"* ]] && [ -d "${PROJECT_ROOT}/.venv" ]; then + QUARANTINED=$(find "${PROJECT_ROOT}/.venv" -name '*.so' -exec /usr/bin/xattr -l {} \; 2>/dev/null | grep -c 'com.apple.quarantine' || true) + if [ "${QUARANTINED}" -gt 0 ]; then + fail "${QUARANTINED} .so file(s) in .venv are quarantined by macOS Gatekeeper" + fix "Run: npm run setup (it clears quarantine automatically)" + else + ok "No quarantined files in .venv" + fi +fi + +# ============================================================================= +# 4. Environment file +# ============================================================================= +echo "" +echo -e "${BOLD}Environment Configuration${NC}" + +if [ -f "${PROJECT_ROOT}/.env" ]; then + ok ".env file exists" + + # Check critical keys + SECRET_KEY=$(grep '^SECRET_KEY=' "${PROJECT_ROOT}/.env" | cut -d= -f2-) + if [ "${SECRET_KEY}" = "your-secret-key-here" ] || [ -z "${SECRET_KEY}" ]; then + fail "SECRET_KEY is not configured" + fix "Run: npm run setup (it generates secrets automatically)" + else + ok "SECRET_KEY is set" + fi + + ENV_MODE=$(grep '^ENVIRONMENT=' "${PROJECT_ROOT}/.env" | cut -d= -f2-) + if [ "${ENV_MODE}" = "development" ]; then + ok "ENVIRONMENT=development" + else + warn "ENVIRONMENT=${ENV_MODE} (expected 'development' for local dev)" + fix "Set ENVIRONMENT=development in .env" + fi +else + fail ".env file is missing" + fix "Run: npm run setup OR cp .env.sample .env" +fi + +# ============================================================================= +# 5. Database +# ============================================================================= +echo "" +echo -e "${BOLD}Database${NC}" + +DB_URL=$(grep '^DATABASE_URL=' "${PROJECT_ROOT}/.env" 2>/dev/null | cut -d= -f2- || echo "") +if [ -z "${DB_URL}" ] || [[ "${DB_URL}" == *"sqlite"* ]]; then + if [ -f "${PROJECT_ROOT}/db.sqlite3" ]; then + DB_SIZE=$(du -h "${PROJECT_ROOT}/db.sqlite3" | cut -f1) + ok "SQLite database exists (${DB_SIZE})" + else + warn "SQLite database does not exist yet" + fix "Run: npm run setup (it runs migrations automatically)" + fi +else + ok "DATABASE_URL is configured (non-SQLite)" +fi + +# ============================================================================= +# 6. Port 8000 +# ============================================================================= +echo "" +echo -e "${BOLD}Network${NC}" + +if command -v lsof &>/dev/null; then + PORT_PID=$(lsof -Pi :8000 -sTCP:LISTEN -t 2>/dev/null || echo "") + if [ -n "${PORT_PID}" ]; then + PORT_CMD=$(ps -p "${PORT_PID}" -o comm= 2>/dev/null || echo "unknown") + fail "Port 8000 is in use by PID ${PORT_PID} (${PORT_CMD})" + fix "Kill it with: kill ${PORT_PID}" + else + ok "Port 8000 is available" + fi +elif command -v ss &>/dev/null; then + if ss -tlnp 2>/dev/null | grep -q ':8000 '; then + fail "Port 8000 is in use" + fix "Find the process: ss -tlnp | grep :8000 then kill it" + else + ok "Port 8000 is available" + fi +else + warn "Cannot check port 8000 (lsof/ss not available)" +fi + +# ============================================================================= +# 7. Docker (optional) +# ============================================================================= +echo "" +echo -e "${BOLD}Docker (optional)${NC}" + +if command -v docker &>/dev/null; then + if docker info &>/dev/null 2>&1; then + ok "Docker is running" + else + warn "Docker is installed but not running" + fix "Start Docker Desktop or run: sudo systemctl start docker" + fi +else + warn "Docker is not installed (optional — needed for docker-compose.dev.yml)" + fix "Install from https://docs.docker.com/get-docker/" +fi + +# ============================================================================= +# 8. Redis (optional) +# ============================================================================= +echo "" +echo -e "${BOLD}Redis (optional — for WebSocket features)${NC}" + +if command -v redis-cli &>/dev/null; then + if redis-cli ping &>/dev/null 2>&1; then + REDIS_VER=$(redis-cli info server 2>/dev/null | grep redis_version | cut -d: -f2 | tr -d '\r') + ok "Redis is running (${REDIS_VER})" + else + warn "Redis CLI found but server is not reachable" + fix "Start Redis: redis-server OR docker run -d -p 6379:6379 redis:7-alpine" + fi +else + warn "Redis is not installed" + fix "brew install redis OR docker run -d -p 6379:6379 redis:7-alpine" +fi + +# ============================================================================= +# Summary +# ============================================================================= +echo "" +echo -e "${BOLD}─────────────────────────────────────────────────${NC}" +TOTAL=$((PASS + WARN + FAIL)) +echo -e " Results: ${GREEN}${PASS} passed${NC}, ${YELLOW}${WARN} warnings${NC}, ${RED}${FAIL} failed${NC} (${TOTAL} checks)" +echo "" + +if [ "${FAIL}" -gt 0 ]; then + echo -e " ${RED}${BOLD}Some checks failed.${NC} Fix the issues above and run ${BOLD}npm run doctor${NC} again." + exit 1 +elif [ "${WARN}" -gt 0 ]; then + echo -e " ${YELLOW}${BOLD}Looks mostly good!${NC} Warnings are optional but recommended to fix." + exit 0 +else + echo -e " ${GREEN}${BOLD}Everything looks great! 🎉${NC}" + exit 0 +fi diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 000000000..24b25a056 --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +# ============================================================================= +# setup.sh — Idempotent local development setup for Alpha One Labs +# +# Usage: npm run setup OR bash scripts/setup.sh +# +# This script is safe to run repeatedly. It will never overwrite user files +# (.env, db.sqlite3) and will skip steps that are already completed. +# +# Requirements: Python 3.10+, pip (Poetry is installed automatically if missing) +# Optional: Docker (for Redis/MySQL), Redis CLI +# ============================================================================= +set -euo pipefail + +# -- Colours & helpers -------------------------------------------------------- +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m' +BOLD='\033[1m'; NC='\033[0m' + +info() { echo -e "${BLUE}${BOLD}$1${NC}"; } +ok() { echo -e "${GREEN} ✔ $1${NC}"; } +warn() { echo -e "${YELLOW} ⚠ $1${NC}"; } +fail() { echo -e "${RED} ✖ $1${NC}"; } +die() { fail "$1"; echo -e "${RED} → $2${NC}"; exit 1; } + +TOTAL_STEPS=7 +step() { echo -e "\n${BOLD}[$1/${TOTAL_STEPS}] $2${NC}"; } + +# -- Resolve project root (script lives in /scripts/) ------------------- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +cd "${PROJECT_ROOT}" + +# ============================================================================= +# Step 1 — Check required software versions +# ============================================================================= +step 1 "Checking dependencies..." + +# Python ≥ 3.10 +if ! command -v python3 &>/dev/null; then + die "Python 3 is not installed." \ + "Install Python 3.10+ from https://www.python.org/downloads/" +fi + +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +PYTHON_MAJOR=$(echo "${PYTHON_VERSION}" | cut -d. -f1) +PYTHON_MINOR=$(echo "${PYTHON_VERSION}" | cut -d. -f2) + +if [ "${PYTHON_MAJOR}" -lt 3 ] || { [ "${PYTHON_MAJOR}" -eq 3 ] && [ "${PYTHON_MINOR}" -lt 10 ]; }; then + die "Python ${PYTHON_VERSION} found, but 3.10+ is required." \ + "Install Python 3.10+ from https://www.python.org/downloads/" +fi +ok "Python ${PYTHON_VERSION}" + +# pip +if ! python3 -m pip --version &>/dev/null; then + die "pip is not available." \ + "Run: python3 -m ensurepip --upgrade" +fi +ok "pip available" + +# Poetry (install automatically if missing, and handle PATH issues) +POETRY_CMD="" +if command -v poetry &>/dev/null; then + POETRY_CMD="poetry" +else + warn "Poetry not found — installing Poetry 1.8.3..." + python3 -m pip install --quiet poetry==1.8.3 2>&1 || true + + # pip may install the binary to a user-local scripts dir not on PATH. + # Common locations: ~/.local/bin (Linux), ~/Library/Python/X.Y/bin (macOS) + if command -v poetry &>/dev/null; then + POETRY_CMD="poetry" + else + # Search common pip script directories + for candidate in \ + "$HOME/.local/bin/poetry" \ + "$HOME/Library/Python/${PYTHON_VERSION}/bin/poetry" \ + "$(python3 -c 'import sysconfig; print(sysconfig.get_path("scripts", "posix_user"))' 2>/dev/null)/poetry" + do + if [ -x "${candidate}" ]; then + export PATH="$(dirname "${candidate}"):${PATH}" + POETRY_CMD="poetry" + ok "Added $(dirname "${candidate}") to PATH" + break + fi + done + + # Last resort: run poetry as a Python module + if [ -z "${POETRY_CMD}" ] && python3 -c "import poetry" &>/dev/null; then + POETRY_CMD="python3 -m poetry" + ok "Using poetry via: python3 -m poetry" + fi + fi +fi + +if [ -z "${POETRY_CMD}" ]; then + die "Could not find or install Poetry." \ + "Install manually: pip install poetry==1.8.3 and ensure it's on your PATH" +fi + +POETRY_VERSION=$(${POETRY_CMD} --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') +ok "Poetry ${POETRY_VERSION}" + +# Optional: Docker +if command -v docker &>/dev/null; then + ok "Docker available (optional)" +else + warn "Docker not found — optional, needed only for docker-compose.dev.yml" +fi + +# Optional: Redis CLI +if command -v redis-cli &>/dev/null; then + ok "Redis CLI available (optional)" +else + warn "Redis CLI not found — optional, WebSocket features need Redis at runtime" +fi + +# ============================================================================= +# Step 2 — Install Python dependencies via Poetry +# ============================================================================= +step 2 "Installing packages..." + +# Ensure Poetry creates a virtualenv (override the repo's poetry.toml which +# sets create=false — that setting is intended for Docker/CI, not local dev). +poetry config virtualenvs.in-project true --local 2>/dev/null || true + +# mysqlclient requires MySQL C headers (mysql_config) to compile. +# On macOS, these come from `brew install mysql-client`. +# For SQLite-only local dev, mysqlclient is not needed at all. +if ! command -v mysql_config &>/dev/null; then + if [[ "$OSTYPE" == "darwin"* ]] && command -v brew &>/dev/null; then + warn "mysql_config not found — trying: brew install mysql-client" + if brew install mysql-client 2>/dev/null; then + MYSQL_PREFIX="$(brew --prefix mysql-client 2>/dev/null)" + export PKG_CONFIG_PATH="${MYSQL_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" + export PATH="${MYSQL_PREFIX}/bin:${PATH}" + ok "mysql-client installed via Homebrew" + else + warn "brew install mysql-client failed — will skip mysqlclient package" + fi + else + warn "mysql_config not found — mysqlclient will be skipped (not needed for SQLite)" + fi +fi + +# Try poetry install; if it fails (usually due to mysqlclient), fall back to +# installing everything except mysqlclient via pip. +if poetry install --no-interaction --no-ansi 2>&1 | tail -5; then + ok "Python dependencies installed" +else + warn "poetry install failed (likely mysqlclient). Falling back to pip install..." + + # Create the venv manually if poetry didn't + if [ ! -d "${PROJECT_ROOT}/.venv" ]; then + python3 -m venv "${PROJECT_ROOT}/.venv" + fi + PIP="${PROJECT_ROOT}/.venv/bin/pip" + + # Export requirements from poetry, remove mysqlclient, install the rest + poetry export --without-hashes --no-interaction 2>/dev/null \ + | grep -v '^mysqlclient' \ + > "${PROJECT_ROOT}/.tmp-requirements.txt" + "${PIP}" install --quiet -r "${PROJECT_ROOT}/.tmp-requirements.txt" + rm -f "${PROJECT_ROOT}/.tmp-requirements.txt" + + # Also install dev dependencies + poetry export --with dev --without-hashes --no-interaction 2>/dev/null \ + | grep -v '^mysqlclient' \ + > "${PROJECT_ROOT}/.tmp-dev-requirements.txt" + "${PIP}" install --quiet -r "${PROJECT_ROOT}/.tmp-dev-requirements.txt" + rm -f "${PROJECT_ROOT}/.tmp-dev-requirements.txt" + + ok "Python dependencies installed (mysqlclient skipped — not needed for SQLite)" +fi + +# Detect the poetry venv python for subsequent commands +if [ -d "${PROJECT_ROOT}/.venv" ]; then + PYTHON="${PROJECT_ROOT}/.venv/bin/python" +else + # Fallback: let poetry figure it out + PYTHON="$(poetry env info -e 2>/dev/null || echo python3)" +fi +ok "Using Python: ${PYTHON}" + +# macOS Gatekeeper: Remove quarantine attributes from compiled binaries (.so files) +# like _rust.abi3.so (from cryptography). Without this, macOS may block execution +# with a "Not Opened" security popup. +if [[ "$OSTYPE" == "darwin"* ]] && [ -d "${PROJECT_ROOT}/.venv" ]; then + find "${PROJECT_ROOT}/.venv" -type f \( -name '*.so' -o -name '*.dylib' \) \ + -exec /usr/bin/xattr -d com.apple.quarantine {} \; 2>/dev/null || true + ok "Cleared macOS quarantine flags on .venv binaries" +fi + +# ============================================================================= +# Step 3 — Create .env from .env.sample safely +# ============================================================================= +step 3 "Configuring environment..." + +if [ -f "${PROJECT_ROOT}/.env" ]; then + ok ".env already exists — not overwriting" +else + if [ ! -f "${PROJECT_ROOT}/.env.sample" ]; then + die ".env.sample is missing from the repository." \ + "Ensure you have a clean checkout." + fi + cp "${PROJECT_ROOT}/.env.sample" "${PROJECT_ROOT}/.env" + ok "Created .env from .env.sample" +fi + +# ============================================================================= +# Step 4 — Generate secure random secrets +# ============================================================================= +step 4 "Generating secrets..." + +# Generate a Django SECRET_KEY if it still has the placeholder value +CURRENT_SECRET=$(grep '^SECRET_KEY=' "${PROJECT_ROOT}/.env" | cut -d= -f2-) +if [ "${CURRENT_SECRET}" = "your-secret-key-here" ] || [ -z "${CURRENT_SECRET}" ]; then + NEW_SECRET=$("${PYTHON}" -c " +import secrets, string +chars = string.ascii_letters + string.digits + '!@#\$%^&*(-_=+)' +print(''.join(secrets.choice(chars) for _ in range(50))) +") + # Use a delimiter that won't conflict with the secret value + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^SECRET_KEY=.*|SECRET_KEY=${NEW_SECRET}|" "${PROJECT_ROOT}/.env" + else + sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${NEW_SECRET}|" "${PROJECT_ROOT}/.env" + fi + ok "Generated new SECRET_KEY" +else + ok "SECRET_KEY already set" +fi + +# Generate MESSAGE_ENCRYPTION_KEY if it still has the sample value +CURRENT_ENCRYPTION_KEY=$(grep '^MESSAGE_ENCRYPTION_KEY=' "${PROJECT_ROOT}/.env" | cut -d= -f2-) +SAMPLE_KEY="5ezrkqK2lhifqBRt9f8_dZhFQF_f5ipSQDV8Vzv9Dek=" +if [ "${CURRENT_ENCRYPTION_KEY}" = "${SAMPLE_KEY}" ] || [ -z "${CURRENT_ENCRYPTION_KEY}" ]; then + NEW_ENCRYPTION_KEY=$("${PYTHON}" -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${NEW_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" + else + sed -i "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${NEW_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" + fi + ok "Generated new MESSAGE_ENCRYPTION_KEY" +else + ok "MESSAGE_ENCRYPTION_KEY already set" +fi + +# Ensure development mode +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^ENVIRONMENT=.*|ENVIRONMENT=development|" "${PROJECT_ROOT}/.env" + sed -i '' "s|^DEBUG=.*|DEBUG=True|" "${PROJECT_ROOT}/.env" + sed -i '' "s|^DATABASE_URL=.*|DATABASE_URL=sqlite:///db.sqlite3|" "${PROJECT_ROOT}/.env" +else + sed -i "s|^ENVIRONMENT=.*|ENVIRONMENT=development|" "${PROJECT_ROOT}/.env" + sed -i "s|^DEBUG=.*|DEBUG=True|" "${PROJECT_ROOT}/.env" + sed -i "s|^DATABASE_URL=.*|DATABASE_URL=sqlite:///db.sqlite3|" "${PROJECT_ROOT}/.env" +fi +ok "Environment set to development with SQLite" + +# ============================================================================= +# Step 5 — Run migrations safely +# ============================================================================= +step 5 "Running database migrations..." + +"${PYTHON}" manage.py migrate --no-input 2>&1 | tail -3 +ok "Migrations complete" + +# ============================================================================= +# Step 6 — Seed minimal demo data +# ============================================================================= +step 6 "Seeding demo data..." + +# create_test_data is idempotent — it checks for existing data internally +"${PYTHON}" manage.py create_test_data 2>&1 | tail -5 +ok "Demo data seeded" + +# ============================================================================= +# Step 7 — Verify app boots successfully +# ============================================================================= +step 7 "Verifying application..." + +"${PYTHON}" manage.py check --deploy 2>&1 | tail -3 || true +# The --deploy check may warn about HTTPS settings in dev; that's expected. +# The important thing is that the app loads without ImportError / config issues. +"${PYTHON}" manage.py check 2>&1 +ok "Django system check passed" + +# -- Done! -------------------------------------------------------------------- +echo "" +echo -e "${GREEN}${BOLD}══════════════════════════════════════════════════════════${NC}" +echo -e "${GREEN}${BOLD} ✔ Setup complete!${NC}" +echo -e "${GREEN}${BOLD}══════════════════════════════════════════════════════════${NC}" +echo "" +echo -e " Start the dev server: ${BOLD}npm run dev${NC}" +echo -e " Run diagnostics: ${BOLD}npm run doctor${NC}" +echo -e " Run tests: ${BOLD}npm run test${NC}" +echo "" +echo -e " Admin login: admin@example.com / adminpassword" +echo -e " Dev server URL: http://localhost:8000" +echo "" diff --git a/web/views.py b/web/views.py index 8dd972d98..56f41acea 100644 --- a/web/views.py +++ b/web/views.py @@ -221,11 +221,6 @@ def handle_referral(request, code): """Handle referral link with the format /en/ref/CODE/ and redirect to homepage.""" # Store referral code in session request.session["referral_code"] = code - - # The WebRequestMiddleware will automatically log this request with the correct path - # containing the referral code, so we don't need to create a WebRequest manually - - # Redirect to homepage return redirect("index") @@ -8772,12 +8767,11 @@ def get_context_data(self, **kwargs): class SurveyDeleteView(LoginRequiredMixin, DeleteView): model = Survey - success_url = reverse_lazy("surveys") # Use reverse_lazy + success_url = reverse_lazy("surveys") template_name = "surveys/delete.html" login_url = "/accounts/login/" def get_queryset(self): - # Override queryset to only allow creator to access the survey for deletion base_qs = super().get_queryset() return base_qs.filter(author=self.request.user) @@ -8791,17 +8785,14 @@ def join_session_waiting_room(request, course_slug): """View for joining a session waiting room for the next session of a course.""" course = get_object_or_404(Course, slug=course_slug) - # Get or create the session waiting room for this course session_waiting_room, created = WaitingRoom.objects.get_or_create( course=course, status="open", defaults={"status": "open"} ) - # Check if the waiting room is open if session_waiting_room.status != "open": messages.error(request, "This session waiting room is no longer open for joining.") return redirect("course_detail", slug=course_slug) - # Add the user to participants if not already in if request.user not in session_waiting_room.participants.all(): session_waiting_room.participants.add(request.user) next_session = session_waiting_room.get_next_session() From bcdd18269819c4fbc6d1a74d26bf79a73c1799fc Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:01:21 +0530 Subject: [PATCH 02/14] fix(devcontainer): strip comments to fix JSON linting --- .devcontainer/devcontainer.json | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 99da62351..dc7254400 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,27 +1,15 @@ -// ============================================================================= -// VS Code Dev Container configuration -// -// Opens this project inside a Docker container with all services running. -// Uses docker-compose.dev.yml for MySQL + Redis alongside the web container. -// -// Prerequisites: Docker Desktop + VS Code "Dev Containers" extension -// ============================================================================= { "name": "Alpha One Labs — Dev", "dockerComposeFile": "../docker-compose.dev.yml", "service": "web", "workspaceFolder": "/app", - // Forward the Django dev server port to the host "forwardPorts": [ 8000, 3306, 6379 ], - // Run after the container is created (first time only) "postCreateCommand": "pip install poetry==1.8.3 && poetry config virtualenvs.create false --local && poetry install --no-interaction", - // Run every time the container starts "postStartCommand": "python manage.py migrate --no-input && python manage.py collectstatic --noinput", - // VS Code settings inside the container "customizations": { "vscode": { "settings": { @@ -44,7 +32,6 @@ ] } }, - // Features to install in the container "features": { "ghcr.io/devcontainers/features/git:1": {} } From 0292d531874086c7ab96731cc68e4a24f9dbbf6d Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:05:21 +0530 Subject: [PATCH 03/14] ci: run smoke test on .env.sample changes --- .github/workflows/onboarding-smoke-test.yml | 28 +++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml index c5eccaaff..1e50c0d7a 100644 --- a/.github/workflows/onboarding-smoke-test.yml +++ b/.github/workflows/onboarding-smoke-test.yml @@ -13,18 +13,20 @@ on: push: branches: [main] paths: - - 'scripts/**' - - 'package.json' - - 'pyproject.toml' - - 'poetry.lock' - - '.github/workflows/onboarding-smoke-test.yml' + - "scripts/**" + - "package.json" + - "pyproject.toml" + - "poetry.lock" + - ".env.sample" + - ".github/workflows/onboarding-smoke-test.yml" pull_request: paths: - - 'scripts/**' - - 'package.json' - - 'pyproject.toml' - - 'poetry.lock' - - '.github/workflows/onboarding-smoke-test.yml' + - "scripts/**" + - "package.json" + - "pyproject.toml" + - "poetry.lock" + - ".env.sample" + - ".github/workflows/onboarding-smoke-test.yml" workflow_dispatch: permissions: @@ -42,13 +44,13 @@ jobs: - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: '3.10' - cache: 'pip' + python-version: "3.10" + cache: "pip" - name: Set up Node.js (for npm run commands) uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Run setup script run: | From f368b7286ffd73c7c60626b77946aa22c45309e0 Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:06:36 +0530 Subject: [PATCH 04/14] ci: fail the job if npm run doctor fails --- .github/workflows/onboarding-smoke-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml index 1e50c0d7a..bd9716514 100644 --- a/.github/workflows/onboarding-smoke-test.yml +++ b/.github/workflows/onboarding-smoke-test.yml @@ -60,7 +60,7 @@ jobs: - name: Run doctor run: | chmod +x scripts/doctor.sh - npm run doctor || true # Warnings are OK in CI (e.g. no Docker) + npm run doctor - name: Boot server and hit health endpoint run: | From b4e838214bc966a7eb1b988e6726b3705b50b42b Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:07:45 +0530 Subject: [PATCH 05/14] ci: quote SERVER_PID in bash scripts to prevent word splitting --- .github/workflows/onboarding-smoke-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml index bd9716514..7eda393e8 100644 --- a/.github/workflows/onboarding-smoke-test.yml +++ b/.github/workflows/onboarding-smoke-test.yml @@ -79,7 +79,7 @@ jobs: fi if [ $i -eq 30 ]; then echo "Server failed to start within 30 seconds" - kill $SERVER_PID 2>/dev/null || true + kill "${SERVER_PID}" 2>/dev/null || true exit 1 fi sleep 1 @@ -93,11 +93,11 @@ jobs: echo "✔ Health check passed!" else echo "✖ Health check failed with HTTP ${HTTP_CODE}" - kill $SERVER_PID 2>/dev/null || true + kill "${SERVER_PID}" 2>/dev/null || true exit 1 fi # Clean shutdown - kill $SERVER_PID 2>/dev/null || true - wait $SERVER_PID 2>/dev/null || true + kill "${SERVER_PID}" 2>/dev/null || true + wait "${SERVER_PID}" 2>/dev/null || true echo "✔ Server stopped cleanly" From 3e55b3f1b8651e88e57b9acbe6dc49451365e42c Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:09:42 +0530 Subject: [PATCH 06/14] fix(setup): escape sed special characters in generated secrets --- scripts/setup.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/setup.sh b/scripts/setup.sh index 24b25a056..97f176b9d 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -220,11 +220,14 @@ import secrets, string chars = string.ascii_letters + string.digits + '!@#\$%^&*(-_=+)' print(''.join(secrets.choice(chars) for _ in range(50))) ") + # Escape special characters (&, \, and |) for the sed replacement string + ESCAPED_SECRET=$(printf '%s\n' "$NEW_SECRET" | sed -e 's/[\/&]/\\&/g') + # Use a delimiter that won't conflict with the secret value if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^SECRET_KEY=.*|SECRET_KEY=${NEW_SECRET}|" "${PROJECT_ROOT}/.env" + sed -i '' "s|^SECRET_KEY=.*|SECRET_KEY=${ESCAPED_SECRET}|" "${PROJECT_ROOT}/.env" else - sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${NEW_SECRET}|" "${PROJECT_ROOT}/.env" + sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${ESCAPED_SECRET}|" "${PROJECT_ROOT}/.env" fi ok "Generated new SECRET_KEY" else @@ -236,10 +239,13 @@ CURRENT_ENCRYPTION_KEY=$(grep '^MESSAGE_ENCRYPTION_KEY=' "${PROJECT_ROOT}/.env" SAMPLE_KEY="5ezrkqK2lhifqBRt9f8_dZhFQF_f5ipSQDV8Vzv9Dek=" if [ "${CURRENT_ENCRYPTION_KEY}" = "${SAMPLE_KEY}" ] || [ -z "${CURRENT_ENCRYPTION_KEY}" ]; then NEW_ENCRYPTION_KEY=$("${PYTHON}" -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") + # Escape the encryption key in case it contains sed-special characters (though base64 usually doesn't, it's safer) + ESCAPED_ENCRYPTION_KEY=$(printf '%s\n' "$NEW_ENCRYPTION_KEY" | sed -e 's/[\/&]/\\&/g') + if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${NEW_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" + sed -i '' "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${ESCAPED_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" else - sed -i "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${NEW_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" + sed -i "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${ESCAPED_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" fi ok "Generated new MESSAGE_ENCRYPTION_KEY" else From fc21ccacdcce18bcbdc66565697148baa195e58d Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:15:44 +0530 Subject: [PATCH 07/14] ci: commit scripts as executable and remove redundant chmod from workflow --- .github/workflows/onboarding-smoke-test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml index 7eda393e8..3b0892ecf 100644 --- a/.github/workflows/onboarding-smoke-test.yml +++ b/.github/workflows/onboarding-smoke-test.yml @@ -54,17 +54,14 @@ jobs: - name: Run setup script run: | - chmod +x scripts/setup.sh npm run setup - name: Run doctor run: | - chmod +x scripts/doctor.sh npm run doctor - name: Boot server and hit health endpoint run: | - chmod +x scripts/dev.sh # Start the dev server in the background npm run dev & From 06d31a8324b5abedebb477e61c638456439271e8 Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 03:20:33 +0530 Subject: [PATCH 08/14] ci: cache poetry instead of pip --- .github/workflows/onboarding-smoke-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml index 3b0892ecf..bfb8cbb2f 100644 --- a/.github/workflows/onboarding-smoke-test.yml +++ b/.github/workflows/onboarding-smoke-test.yml @@ -45,7 +45,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.10" - cache: "pip" + cache: "poetry" - name: Set up Node.js (for npm run commands) uses: actions/setup-node@v4 From fc653b53f3946501036299e28c03549276c97c7e Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Fri, 27 Feb 2026 10:26:57 +0530 Subject: [PATCH 09/14] Apply PR review codebase fixes: devcontainer, doctor, setup, and dev scripts --- .gitignore | 1 - bash | 5 ----- docker-compose.dev.yml | 16 +++++++++++++--- poetry.toml | 2 +- scripts/dev.sh | 11 ++++++++--- scripts/doctor.sh | 4 ++-- scripts/setup.sh | 24 ++++++++++++++---------- 7 files changed, 38 insertions(+), 25 deletions(-) delete mode 100644 bash diff --git a/.gitignore b/.gitignore index 20822e886..a4ca33898 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,3 @@ education-website-*.json *.sql node_modules/ .venv/ -poetry.toml diff --git a/bash b/bash deleted file mode 100644 index 58caabc4b..000000000 --- a/bash +++ /dev/null @@ -1,5 +0,0 @@ - -[1/7] Checking dependencies... - ✔ Python 3.14 - ✔ pip available - ⚠ Poetry not found — installing Poetry 1.8.3... diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index eefed1956..551641ae7 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -24,7 +24,17 @@ services: volumes: - mysql_data:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-prootpassword"] + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "127.0.0.1", + "-u", + "root", + "-prootpassword", + ] interval: 10s timeout: 5s retries: 5 @@ -55,7 +65,7 @@ services: command: > bash -c " echo 'Waiting for MySQL...' && - while ! mysqladmin ping -h db -u root -prootpassword --silent 2>/dev/null; do + while ! mysqladmin ping -h db -u django -pdjango_password --silent 2>/dev/null; do sleep 1 done && echo 'MySQL is ready!' && @@ -75,7 +85,7 @@ services: ports: - "8000:8000" environment: - - DATABASE_URL=mysql://root:rootpassword@db:3306/education_website + - DATABASE_URL=mysql://django:django_password@db:3306/education_website - REDIS_URL=redis://redis:6379/0 - ENVIRONMENT=development - DEBUG=True diff --git a/poetry.toml b/poetry.toml index 437b344df..53b35d370 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,3 +1,3 @@ [virtualenvs] -create = false +create = true in-project = true diff --git a/scripts/dev.sh b/scripts/dev.sh index 2d986327b..eb395c551 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -57,7 +57,8 @@ if [ -n "${REDIS_URL}" ] && [ "${REDIS_URL}" != "redis://127.0.0.1:6379/0" ]; th # Custom Redis URL configured — warn if it's unreachable if command -v redis-cli &>/dev/null; then if ! redis-cli -u "${REDIS_URL}" ping &>/dev/null 2>&1; then - warn "Redis at ${REDIS_URL} is not reachable." + REDACTED_URL=$(echo "${REDIS_URL}" | sed -E 's|://[^@]+@|://REDACTED@|') + warn "Redis at ${REDACTED_URL} is not reachable." warn "WebSocket features (chat, whiteboard) won't work without Redis." else ok "Redis is reachable" @@ -77,8 +78,12 @@ fi # -- Collect static files (quick, idempotent) --------------------------------- info "Collecting static files..." -"${PYTHON}" manage.py collectstatic --noinput --verbosity=0 2>&1 || true -ok "Static files ready" +if "${PYTHON}" manage.py collectstatic --noinput --verbosity=0 2>&1; then + ok "Static files ready" +else + fail "collectstatic failed. See output above." + exit 1 +fi # -- Trap Ctrl+C for clean shutdown ------------------------------------------- cleanup() { diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 5a24c6e77..ee174aaa1 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -22,7 +22,7 @@ PASS=0; WARN=0; FAIL=0 # -- Resolve project root ----------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -cd "${PROJECT_ROOT}" +cd "${PROJECT_ROOT}" || { echo "Failed to cd into ${PROJECT_ROOT}"; exit 1; } echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}" @@ -39,7 +39,7 @@ if command -v python3 &>/dev/null; then PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")') PY_MAJOR=$(python3 -c 'import sys; print(sys.version_info.major)') PY_MINOR=$(python3 -c 'import sys; print(sys.version_info.minor)') - if [ "${PY_MAJOR}" -ge 3 ] && [ "${PY_MINOR}" -ge 10 ]; then + if [ "${PY_MAJOR}" -gt 3 ] || { [ "${PY_MAJOR}" -eq 3 ] && [ "${PY_MINOR}" -ge 10 ]; }; then ok "Python ${PY_VER} (≥ 3.10 required)" else fail "Python ${PY_VER} found — 3.10+ is required" diff --git a/scripts/setup.sh b/scripts/setup.sh index 97f176b9d..9728d7839 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -63,8 +63,8 @@ POETRY_CMD="" if command -v poetry &>/dev/null; then POETRY_CMD="poetry" else - warn "Poetry not found — installing Poetry 1.8.3..." - python3 -m pip install --quiet poetry==1.8.3 2>&1 || true + warn "Poetry not found — installing Poetry 2.3.2..." + python3 -m pip install --quiet poetry==2.3.2 2>&1 || true # pip may install the binary to a user-local scripts dir not on PATH. # Common locations: ~/.local/bin (Linux), ~/Library/Python/X.Y/bin (macOS) @@ -95,12 +95,16 @@ fi if [ -z "${POETRY_CMD}" ]; then die "Could not find or install Poetry." \ - "Install manually: pip install poetry==1.8.3 and ensure it's on your PATH" + "Install manually: pip install poetry==2.3.2 and ensure it's on your PATH" fi POETRY_VERSION=$(${POETRY_CMD} --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') ok "Poetry ${POETRY_VERSION}" +if [ "$(echo "${POETRY_VERSION}" | cut -d. -f1)" -ge 2 ]; then + "${POETRY_CMD}" self add poetry-plugin-export 2>/dev/null || true +fi + # Optional: Docker if command -v docker &>/dev/null; then ok "Docker available (optional)" @@ -122,7 +126,7 @@ step 2 "Installing packages..." # Ensure Poetry creates a virtualenv (override the repo's poetry.toml which # sets create=false — that setting is intended for Docker/CI, not local dev). -poetry config virtualenvs.in-project true --local 2>/dev/null || true +"${POETRY_CMD}" config virtualenvs.in-project true --local 2>/dev/null || true # mysqlclient requires MySQL C headers (mysql_config) to compile. # On macOS, these come from `brew install mysql-client`. @@ -145,7 +149,7 @@ fi # Try poetry install; if it fails (usually due to mysqlclient), fall back to # installing everything except mysqlclient via pip. -if poetry install --no-interaction --no-ansi 2>&1 | tail -5; then +if "${POETRY_CMD}" install --no-interaction --no-ansi 2>&1 | tail -5; then ok "Python dependencies installed" else warn "poetry install failed (likely mysqlclient). Falling back to pip install..." @@ -157,14 +161,14 @@ else PIP="${PROJECT_ROOT}/.venv/bin/pip" # Export requirements from poetry, remove mysqlclient, install the rest - poetry export --without-hashes --no-interaction 2>/dev/null \ + "${POETRY_CMD}" export --without-hashes --no-interaction 2>/dev/null \ | grep -v '^mysqlclient' \ > "${PROJECT_ROOT}/.tmp-requirements.txt" "${PIP}" install --quiet -r "${PROJECT_ROOT}/.tmp-requirements.txt" rm -f "${PROJECT_ROOT}/.tmp-requirements.txt" # Also install dev dependencies - poetry export --with dev --without-hashes --no-interaction 2>/dev/null \ + "${POETRY_CMD}" export --with dev --without-hashes --no-interaction 2>/dev/null \ | grep -v '^mysqlclient' \ > "${PROJECT_ROOT}/.tmp-dev-requirements.txt" "${PIP}" install --quiet -r "${PROJECT_ROOT}/.tmp-dev-requirements.txt" @@ -178,7 +182,7 @@ if [ -d "${PROJECT_ROOT}/.venv" ]; then PYTHON="${PROJECT_ROOT}/.venv/bin/python" else # Fallback: let poetry figure it out - PYTHON="$(poetry env info -e 2>/dev/null || echo python3)" + PYTHON="$("${POETRY_CMD}" env info -e 2>/dev/null || echo python3)" fi ok "Using Python: ${PYTHON}" @@ -221,7 +225,7 @@ chars = string.ascii_letters + string.digits + '!@#\$%^&*(-_=+)' print(''.join(secrets.choice(chars) for _ in range(50))) ") # Escape special characters (&, \, and |) for the sed replacement string - ESCAPED_SECRET=$(printf '%s\n' "$NEW_SECRET" | sed -e 's/[\/&]/\\&/g') + ESCAPED_SECRET=$(printf '%s\n' "$NEW_SECRET" | sed -e 's/[\\&|]/\\&/g') # Use a delimiter that won't conflict with the secret value if [[ "$OSTYPE" == "darwin"* ]]; then @@ -240,7 +244,7 @@ SAMPLE_KEY="5ezrkqK2lhifqBRt9f8_dZhFQF_f5ipSQDV8Vzv9Dek=" if [ "${CURRENT_ENCRYPTION_KEY}" = "${SAMPLE_KEY}" ] || [ -z "${CURRENT_ENCRYPTION_KEY}" ]; then NEW_ENCRYPTION_KEY=$("${PYTHON}" -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") # Escape the encryption key in case it contains sed-special characters (though base64 usually doesn't, it's safer) - ESCAPED_ENCRYPTION_KEY=$(printf '%s\n' "$NEW_ENCRYPTION_KEY" | sed -e 's/[\/&]/\\&/g') + ESCAPED_ENCRYPTION_KEY=$(printf '%s\n' "$NEW_ENCRYPTION_KEY" | sed -e 's/[\\&|]/\\&/g') if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${ESCAPED_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" From bb869b331f8345f10155dc4de7fa9a6299cb0015 Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Sun, 1 Mar 2026 09:07:07 +0530 Subject: [PATCH 10/14] bug fixes --- .github/workflows/test.yml | 33 ++++++++++++++++++--------------- dev_output.txt | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 dev_output.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 188d1d893..d39d1c6b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,17 +17,16 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Prepare pip cache dir run: mkdir -p ~/.cache/pip - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.10' - cache: 'pip' + python-version: "3.10" + cache: "pip" - name: Install Poetry and dependencies (lint env) run: | @@ -39,7 +38,6 @@ jobs: pre-commit install pre-commit run --all-files - tests: name: Run Tests runs-on: ubuntu-latest @@ -61,16 +59,16 @@ jobs: --health-retries=3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Prepare pip cache dir run: mkdir -p ~/.cache/pip - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.10' - cache: 'pip' + python-version: "3.10" + cache: "pip" - name: Configure MySQL and run tests (Poetry env) env: @@ -84,7 +82,7 @@ jobs: DJANGO_SETTINGS_MODULE: web.settings run: | cp .env.sample .env - sed -i 's|DATABASE_URL=.*|DATABASE_URL=${DATABASE_URL}|g' .env + sed -i "s|DATABASE_URL=.*|DATABASE_URL=${DATABASE_URL}|g" .env sudo apt-get update sudo apt-get install -y default-libmysqlclient-dev @@ -103,13 +101,18 @@ jobs: name: Security Scan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Prepare pip cache dir run: mkdir -p ~/.cache/pip - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.10' - cache: 'pip' + python-version: "3.10" + cache: "pip" + + - name: Run Bandit Security Scan + run: | + pip install bandit==1.7.5 + bandit -r . -ll -ii -x ./venv,.venv,./tests diff --git a/dev_output.txt b/dev_output.txt new file mode 100644 index 000000000..fbe2b45c3 --- /dev/null +++ b/dev_output.txt @@ -0,0 +1,23 @@ + +> alphaonelabs-education-website@1.0.0 dev +> bash scripts/dev.sh + +Pre-flight checks... + ✔ .env file found + ✔ Python: /Users/lakshyagupta/Desktop/GSOC'26/OSL/alphaonelabs-education-website/.venv/bin/python + ⚠ Redis CLI not found — skipping Redis check. + ⚠ WebSocket features (chat, whiteboard) require Redis at runtime. +Collecting static files... +Sentry DSN not configured; error events will not be sent. +Using console email backend with Slack notifications for development +Warning: Service account file not found at /Users/lakshyagupta/Desktop/GSOC'26/OSL/alphaonelabs-education-website/your-service-account-file-path + ✔ Static files ready + +══════════════════════════════════════════════════════════ + Alpha One Labs — Development Server +══════════════════════════════════════════════════════════ + + Local: http://localhost:8000 + Network: http://0.0.0.0:8000 + Admin: http://localhost:8000/a-dmin-url123/ + From dc95c668c54c585588530e2cb9a90ddadcf3e84a Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Mon, 2 Mar 2026 01:45:11 +0530 Subject: [PATCH 11/14] fix(ci): run checks on all pull requests --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d39d1c6b0..637019022 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + types: [opened, synchronize, reopened] merge_group: permissions: From db19a15d800be173275bfe59ea8dc5028608c5e5 Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Mon, 2 Mar 2026 01:48:58 +0530 Subject: [PATCH 12/14] chore: trigger CI again From 30e66433a727c3f58d0dcb10293224b865a32816 Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Mon, 2 Mar 2026 01:55:44 +0530 Subject: [PATCH 13/14] chore: trigger CI via pr comment --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 637019022..f413c908c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,3 +117,5 @@ jobs: run: | pip install bandit==1.7.5 bandit -r . -ll -ii -x ./venv,.venv,./tests + +# Trigger CI From 522d607fa8bcc18a45c791a019c57b543b9c0c3e Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Mon, 2 Mar 2026 11:30:07 +0530 Subject: [PATCH 14/14] Fix trailing whitespace causing linting errors --- .devcontainer/devcontainer.json | 2 +- dev_output.txt | 1 - scripts/setup.sh | 4 ++-- web/views.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dc7254400..c7231d5a3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -35,4 +35,4 @@ "features": { "ghcr.io/devcontainers/features/git:1": {} } -} \ No newline at end of file +} diff --git a/dev_output.txt b/dev_output.txt index fbe2b45c3..947ccd25b 100644 --- a/dev_output.txt +++ b/dev_output.txt @@ -20,4 +20,3 @@ Warning: Service account file not found at /Users/lakshyagupta/Desktop/GSOC'26/O Local: http://localhost:8000 Network: http://0.0.0.0:8000 Admin: http://localhost:8000/a-dmin-url123/ - diff --git a/scripts/setup.sh b/scripts/setup.sh index 9728d7839..1c9edcde8 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -226,7 +226,7 @@ print(''.join(secrets.choice(chars) for _ in range(50))) ") # Escape special characters (&, \, and |) for the sed replacement string ESCAPED_SECRET=$(printf '%s\n' "$NEW_SECRET" | sed -e 's/[\\&|]/\\&/g') - + # Use a delimiter that won't conflict with the secret value if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|^SECRET_KEY=.*|SECRET_KEY=${ESCAPED_SECRET}|" "${PROJECT_ROOT}/.env" @@ -245,7 +245,7 @@ if [ "${CURRENT_ENCRYPTION_KEY}" = "${SAMPLE_KEY}" ] || [ -z "${CURRENT_ENCRYPTI NEW_ENCRYPTION_KEY=$("${PYTHON}" -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") # Escape the encryption key in case it contains sed-special characters (though base64 usually doesn't, it's safer) ESCAPED_ENCRYPTION_KEY=$(printf '%s\n' "$NEW_ENCRYPTION_KEY" | sed -e 's/[\\&|]/\\&/g') - + if [[ "$OSTYPE" == "darwin"* ]]; then sed -i '' "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${ESCAPED_ENCRYPTION_KEY}|" "${PROJECT_ROOT}/.env" else diff --git a/web/views.py b/web/views.py index c92d95d7a..91eb76048 100644 --- a/web/views.py +++ b/web/views.py @@ -8762,7 +8762,7 @@ def get_context_data(self, **kwargs): class SurveyDeleteView(LoginRequiredMixin, DeleteView): model = Survey - success_url = reverse_lazy("surveys") + success_url = reverse_lazy("surveys") template_name = "surveys/delete.html" login_url = "/accounts/login/"