From ea4d5eb2859d28cb49f2a63dba8168f8a797c937 Mon Sep 17 00:00:00 2001 From: Lakshya-2440 Date: Thu, 26 Feb 2026 01:25:13 +0530 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/" From 1dbc6c7f77be85371511314a210fcaf385aa663d Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Wed, 11 Mar 2026 01:38:30 +0530 Subject: [PATCH 15/16] Fix stderr piping in dev.sh --- scripts/dev.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/dev.sh b/scripts/dev.sh index eb395c551..7cb1668ba 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -108,5 +108,5 @@ 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|$)" +"${PYTHON}" manage.py runserver 0.0.0.0:8000 2> >(cat >&2) \ + | grep --line-buffered -v -E "^(Watching for file changes with StatReloader|Performing system checks\.\.\.|System check identified no issues|Django version [0-9]|Starting development server at|Quit the server with CONTROL-C\.|$)" From afa962da42c43d1c83db1fddcfa2842c7ce937f7 Mon Sep 17 00:00:00 2001 From: Lakshya Gupta Date: Wed, 11 Mar 2026 02:48:36 +0530 Subject: [PATCH 16/16] chore: consolidate local dev setup to a single scripts/dev.sh file --- .devcontainer/devcontainer.json | 38 -- .github/workflows/onboarding-smoke-test.yml | 100 ---- .github/workflows/test.yml | 36 +- .gitignore | 1 - alphaonelabs-education-website@1.0.0 | 0 dev_output.txt | 22 - docker-compose.dev.yml | 105 ---- package.json | 12 - poetry.toml | 3 +- scripts/dev.sh | 549 +++++++++++++++++++- scripts/doctor.sh | 228 -------- scripts/setup.sh | 311 ----------- web/views.py | 11 +- 13 files changed, 560 insertions(+), 856 deletions(-) delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .github/workflows/onboarding-smoke-test.yml delete mode 100644 alphaonelabs-education-website@1.0.0 delete mode 100644 dev_output.txt delete mode 100644 docker-compose.dev.yml delete mode 100644 package.json delete mode 100755 scripts/doctor.sh delete mode 100755 scripts/setup.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index c7231d5a3..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "Alpha One Labs — Dev", - "dockerComposeFile": "../docker-compose.dev.yml", - "service": "web", - "workspaceFolder": "/app", - "forwardPorts": [ - 8000, - 3306, - 6379 - ], - "postCreateCommand": "pip install poetry==1.8.3 && poetry config virtualenvs.create false --local && poetry install --no-interaction", - "postStartCommand": "python manage.py migrate --no-input && python manage.py collectstatic --noinput", - "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": { - "ghcr.io/devcontainers/features/git:1": {} - } -} diff --git a/.github/workflows/onboarding-smoke-test.yml b/.github/workflows/onboarding-smoke-test.yml deleted file mode 100644 index bfb8cbb2f..000000000 --- a/.github/workflows/onboarding-smoke-test.yml +++ /dev/null @@ -1,100 +0,0 @@ -# ============================================================================= -# 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" - - ".env.sample" - - ".github/workflows/onboarding-smoke-test.yml" - pull_request: - paths: - - "scripts/**" - - "package.json" - - "pyproject.toml" - - "poetry.lock" - - ".env.sample" - - ".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: "poetry" - - - name: Set up Node.js (for npm run commands) - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Run setup script - run: | - npm run setup - - - name: Run doctor - run: | - npm run doctor - - - name: Boot server and hit health endpoint - run: | - - # 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/.github/workflows/test.yml b/.github/workflows/test.yml index f413c908c..188d1d893 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,6 @@ on: branches: - main pull_request: - types: [opened, synchronize, reopened] merge_group: permissions: @@ -18,16 +17,17 @@ jobs: name: Linting runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + + - uses: actions/checkout@v3 - name: Prepare pip cache dir run: mkdir -p ~/.cache/pip - name: Set up Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v4 with: - python-version: "3.10" - cache: "pip" + python-version: '3.10' + cache: 'pip' - name: Install Poetry and dependencies (lint env) run: | @@ -39,6 +39,7 @@ jobs: pre-commit install pre-commit run --all-files + tests: name: Run Tests runs-on: ubuntu-latest @@ -60,16 +61,16 @@ jobs: --health-retries=3 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Prepare pip cache dir run: mkdir -p ~/.cache/pip - name: Set up Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v4 with: - python-version: "3.10" - cache: "pip" + python-version: '3.10' + cache: 'pip' - name: Configure MySQL and run tests (Poetry env) env: @@ -83,7 +84,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 @@ -102,20 +103,13 @@ jobs: name: Security Scan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Prepare pip cache dir run: mkdir -p ~/.cache/pip - name: Set up Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v4 with: - 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 - -# Trigger CI + python-version: '3.10' + cache: 'pip' diff --git a/.gitignore b/.gitignore index a4ca33898..cfe09d577 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ backup.json ansible/inventory.yml education-website-*.json *.sql -node_modules/ .venv/ diff --git a/alphaonelabs-education-website@1.0.0 b/alphaonelabs-education-website@1.0.0 deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev_output.txt b/dev_output.txt deleted file mode 100644 index 947ccd25b..000000000 --- a/dev_output.txt +++ /dev/null @@ -1,22 +0,0 @@ - -> 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/ diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 551641ae7..000000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,105 +0,0 @@ -# ============================================================================= -# 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 django -pdjango_password --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://django:django_password@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 deleted file mode 100644 index e8fa000d2..000000000 --- a/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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 53b35d370..084377a03 100644 --- a/poetry.toml +++ b/poetry.toml @@ -1,3 +1,2 @@ [virtualenvs] -create = true -in-project = true +create = false diff --git a/scripts/dev.sh b/scripts/dev.sh index 7cb1668ba..c572108a4 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -1,35 +1,336 @@ #!/usr/bin/env bash # ============================================================================= -# dev.sh — Start the local development server +# dev.sh - Unified development script for Alpha One Labs # -# 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. +# Usage: +# ./scripts/dev.sh setup - Install dependencies and setup environment +# ./scripts/dev.sh run - Start the local development server +# ./scripts/dev.sh doctor - Diagnose common environment problems # ============================================================================= set -euo pipefail +COMMAND="${1:-run}" + # -- 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}"; } +ok() { echo -e "${GREEN} ✔ $1${NC}"; PASS=$((PASS + 1)); } +warn() { echo -e "${YELLOW} ⚠ $1${NC}"; WARN=$((WARN + 1)); } +fail() { echo -e "${RED} ✖ $1${NC}"; FAIL=$((FAIL + 1)); } +die() { fail "$1"; echo -e "${RED} → $2${NC}"; exit 1; } +fix() { echo -e " ${BLUE}→ Fix:${NC} $1"; } +step() { echo -e "\n${BOLD}[$1/7] $2${NC}"; } + +PASS=0; WARN=0; FAIL=0 -# -- Resolve project root ----------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# If the script is in root (./scripts/dev.sh), SCRIPT_DIR is the project root. +# If the script is in scripts/ (./scripts/dev.sh), project root is .. +if [ -f "${SCRIPT_DIR}/manage.py" ]; then + PROJECT_ROOT="${SCRIPT_DIR}" +else + PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +fi cd "${PROJECT_ROOT}" +# ============================================================================= +# SETUP +# ============================================================================= +function cmd_setup() { +# ============================================================================= +# 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 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) + 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==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)" +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_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`. +# 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_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..." + + # 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_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_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" + 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_CMD}" 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))) +") + # 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" + else + sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${ESCAPED_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())") + # 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 + sed -i "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${ESCAPED_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}./scripts/dev.sh run${NC}" +echo -e " Run diagnostics: ${BOLD}./scripts/dev.sh doctor${NC}" +echo -e " Run tests: ${BOLD}./scripts/dev.sh test${NC}" +echo "" +echo -e " Admin login: admin@example.com / adminpassword" +echo -e " Dev server URL: http://localhost:8000" +echo "" +} + +# ============================================================================= +# RUN +# ============================================================================= +function cmd_run() { # -- 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 + fail ".env file not found. Run './scripts/dev.sh setup' first." + return 1 fi ok ".env file found" @@ -47,7 +348,7 @@ ok "Python: ${PYTHON}" 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." + warn "Run './scripts/dev.sh doctor' for help, or kill the process on port 8000." fi fi @@ -82,7 +383,7 @@ if "${PYTHON}" manage.py collectstatic --noinput --verbosity=0 2>&1; then ok "Static files ready" else fail "collectstatic failed. See output above." - exit 1 + return 1 fi # -- Trap Ctrl+C for clean shutdown ------------------------------------------- @@ -91,7 +392,7 @@ cleanup() { echo -e "${YELLOW}Shutting down...${NC}" # Kill all child processes in this process group kill -- -$$ 2>/dev/null || true - exit 0 + return 0 } trap cleanup INT TERM @@ -110,3 +411,221 @@ echo "" # stays as the last visible output. Request logs still pass through. "${PYTHON}" manage.py runserver 0.0.0.0:8000 2> >(cat >&2) \ | grep --line-buffered -v -E "^(Watching for file changes with StatReloader|Performing system checks\.\.\.|System check identified no issues|Django version [0-9]|Starting development server at|Quit the server with CONTROL-C\.|$)" +} + +# ============================================================================= +# DOCTOR +# ============================================================================= +function cmd_doctor() { + PASS=0; WARN=0; FAIL=0 + +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}" -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" + 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: ./scripts/dev.sh setup" + fi +else + warn "No .venv directory — dependencies may not be installed" + fix "Run: ./scripts/dev.sh 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: ./scripts/dev.sh 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: ./scripts/dev.sh 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: ./scripts/dev.sh 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: ./scripts/dev.sh 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}./scripts/dev.sh doctor${NC} again." + return 1 +elif [ "${WARN}" -gt 0 ]; then + echo -e " ${YELLOW}${BOLD}Looks mostly good!${NC} Warnings are optional but recommended to fix." + return 0 +else + echo -e " ${GREEN}${BOLD}Everything looks great! 🎉${NC}" + return 0 +fi +} + +case "$COMMAND" in + setup) cmd_setup ;; + run) cmd_run ;; + doctor) cmd_doctor ;; + *) echo "Usage: $0 {setup|run|doctor}"; exit 1 ;; +esac diff --git a/scripts/doctor.sh b/scripts/doctor.sh deleted file mode 100755 index ee174aaa1..000000000 --- a/scripts/doctor.sh +++ /dev/null @@ -1,228 +0,0 @@ -#!/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 "Failed to cd into ${PROJECT_ROOT}"; exit 1; } - -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}" -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" - 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 deleted file mode 100755 index 1c9edcde8..000000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,311 +0,0 @@ -#!/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 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) - 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==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)" -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_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`. -# 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_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..." - - # 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_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_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" - 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_CMD}" 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))) -") - # 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" - else - sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${ESCAPED_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())") - # 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 - sed -i "s|^MESSAGE_ENCRYPTION_KEY=.*|MESSAGE_ENCRYPTION_KEY=${ESCAPED_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 91eb76048..b4d485749 100644 --- a/web/views.py +++ b/web/views.py @@ -222,6 +222,11 @@ 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") @@ -8762,11 +8767,12 @@ def get_context_data(self, **kwargs): class SurveyDeleteView(LoginRequiredMixin, DeleteView): model = Survey - success_url = reverse_lazy("surveys") + success_url = reverse_lazy("surveys") # Use reverse_lazy 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) @@ -8780,14 +8786,17 @@ 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()