A brutally engineered, production-ready online judge system for mathematics olympiad practice with battle-tested design patterns, complete audit trails, immutable data, and abuse-resistant architecture.
Unlike naive online judge implementations, MathOJ is designed with:
| Problem | Solution | Why It Matters |
|---|---|---|
| Admin edits problem after submissions | ProblemVersion - immutable snapshots | Students can't be marked wrong for a changed problem |
| Hard deletes corrupt data | Soft deletion everywhere | Foreign keys never break, audit trail preserved |
| Leaderboard changes when recalculated | LeaderboardSnapshot - batch immutable | Historical records are certified, finals are final |
| No evidence of who changed scores | ScoringRecord - immutable audit trail | Appeals, compliance, abuse detection |
| Students exploit timing attacks | Frozen leaderboards during contest | No sniping, no meta-gaming |
| Category changes after submission | Category snapshot on User | Students compete fairly in their original category |
| Same problem submitted by two students | Submission hash + plagiarism detection | Cheat detection built-in |
| Admin has absolute power | Role-based permissions (future) | Single rogue admin can't destroy everything |
accounts/ → User auth, roles, profiles (extends AbstractUser)
problems/ → Problem versioning, edit history (never mutable)
submissions/ → Submission lifecycle, scoring, appeals (immutable)
contests/ → Contest isolation, registration, boundaries (safe scope)
leaderboards/ → Batch snapshots, fairness, bans (never live)
core/ → Audit logging, system config, shared utilities (compliance)
- Immutability First: Once created, only soft-delete, never modify
- Versioning: Problem changes create new version, submissions pin version
- Audit Trail: Every change logged with who, when, why
- Denormalization: Store computed values (scores) separately for safety
- Constraints: Unique constraints prevent duplicates at database level
| Model | Versioned | Immutable | Audit | Purpose |
|---|---|---|---|---|
ProblemVersion |
✓ | ✓ | ✓ | Problem snapshots (v1, v2, v3...) |
Submission |
- | ✓ | - | Student answers (never changed) |
ScoringRecord |
- | ✓ | ✓ | Score history (who changed to what, when, why) |
Contest |
- | - | ✓ | Contest metadata (editable before start) |
LeaderboardSnapshot |
- | ✓ | - | Frozen ranking at point in time |
AuditLog |
- | ✓ | ✓ | EVERY admin action logged |
# Clone repository
git clone <repo> mathoj
cd mathoj
# Python environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Database migration
python manage.py migrate
# Create admin user
python manage.py createsuperuser
# Run development server
python manage.py runserverNavigate to:
- App: http://localhost:8000
- Admin: http://localhost:8000/admin
- API: http://localhost:8000/api/v1 (future)
See DEPLOYMENT_GUIDE.md for complete production setup with:
- PostgreSQL + connection pooling
- Gunicorn + Nginx
- Redis caching + Celery background jobs
- SSL/HTTPS + Let's Encrypt
- Monitoring + error tracking
- Automated backups
- Register with institute & class level
- Browse problems by category/difficulty
- Submit written answers
- Auto-grading with instant feedback
- View submission history
- Appeal scores (dispute incorrect grading)
- View personal statistics
- See leaderboard (frozen, no timing exploits)
- Join contests
- Track progress over time
- Create problems (with LaTeX support)
- Edit problems (creates new version)
- View problem change history
- Mark incorrect auto-grades
- Review student appeals
- Create contests
- Set scoring rules
- Monitor submissions in real-time
- Detect plagiarism
- Generate reports
- Full Django admin interface
- System configuration
- View audit logs
- Manage all contests/problems/users
- Ban users for cheating
- Access detailed analytics
- Backup database
- (Future) Role hierarchy
- Audit logging (every action)
- Soft deletes (never hard delete)
- Immutable data (never overwrite)
- Batch leaderboard snapshots
- Rate limiting (prevent brute force)
- Plagiarism detection (similar answer detection)
- Contest isolation (multiple contests simultaneously)
- Category fairness (students compete in their category)
- Appeal system (dispute grades)
User Model (Abstract User)
├── role: student, teacher, admin
├── category: junior, intermediate, senior (snapshot)
├── is_active: Soft delete flag
└── Permission decorators (future RBAC)
| Attack | Prevention |
|---|---|
| Brute force answers | Rate limiting (5 submissions/day/problem) |
| SQL injection | Django ORM (no raw SQL) |
| XSS | Template escaping + CSP headers |
| CSRF | Django middleware + tokens |
| Timing attacks | Leaderboard frozen + batch snapshots |
| Answer copying | Plagiarism detection (cosine similarity) |
| Admin abuse | Audit logs + role separation (future) |
| Data corruption | Soft deletes + immutable records |
Every action creates AuditLog:
- Who: user_id
- What: action (create, update, delete, score_change)
- When: timestamp
- Where: content_type + object_id
- Why: reason
- Old/new values: JSON diffaccounts_user (extends Django User)
├── phone VARCHAR(20)
├── class_level ENUM
├── institute VARCHAR(255)
├── category ENUM (snapshot, never change retroactively)
├── category_assigned_at TIMESTAMP
├── role ENUM (student, teacher, admin)
└── created_at, updated_at TIMESTAMPproblems_problem_version (IMMUTABLE after created)
├── problem_id VARCHAR(50) -- GEOM-2025-001
├── version_number INT -- 1, 2, 3...
├── title, statement, answer TEXT
├── answer_hash VARCHAR(64) -- SHA256 normalized
├── category ENUM
├── difficulty INT (1-4)
├── is_published, is_active BOOL
├── created_by FK(User)
└── created_at TIMESTAMPsubmissions_submission (IMMUTABLE)
├── user FK(User)
├── problem_version FK(ProblemVersion) -- Pins specific version
├── contest FK(Contest, NULL) -- NULL for practice
├── answer TEXT (student's response)
├── answer_hash VARCHAR(64)
├── state ENUM (draft, submitted, grading, graded, disqualified)
├── is_correct BOOL (NULL until graded)
├── score INT (denormalized from latest ScoringRecord)
├── submission_hash VARCHAR(64) -- For plagiarism detection
├── ip_address, user_agent (fraud detection)
└── submitted_at, graded_at TIMESTAMPsubmissions_scoring_record (IMMUTABLE audit trail)
├── submission FK(Submission)
├── old_score INT (previous score)
├── new_score INT (new score)
├── change_reason ENUM (auto_grading, manual, appeal, plagiarism, etc.)
├── changed_by FK(User) -- Who made the change
├── notes TEXT -- Why changed
└── changed_at TIMESTAMP
-- When new ScoringRecord created:
-- 1. Save immutable record
-- 2. Update Submission.score (denormalized)
-- 3. Create AuditLogcontests_contest
├── name, description VARCHAR
├── category ENUM (junior, intermediate, senior, all)
├── start_time, end_time TIMESTAMP
├── freeze_time TIMESTAMP (leaderboard frozen, submissions still allowed)
├── is_published, show_leaderboard BOOL
├── created_by FK(User)
contests_contest_problem (many-to-many with scoring rules)
├── contest FK(Contest)
├── problem_version FK(ProblemVersion)
├── max_points INT
├── weight DECIMAL (1.0, 0.5, etc)
├── allow_partial_credit BOOL
└── order INTleaderboards_snapshot (IMMUTABLE)
├── contest FK(Contest)
├── snapshot_time TIMESTAMP (when computed)
├── leaderboard_data JSON ([{rank, user_id, username, score}, ...])
├── is_final BOOL (True after contest ends)
├── computed_by VARCHAR ("batch_job", "manual", etc)
-- Computed every 5 minutes during contest
-- Frozen 10 minutes before end
-- No live updates = no timing attackscore_audit_log (IMMUTABLE, NEVER DELETE)
├── user FK(User) -- Who did it
├── action ENUM (create, update, delete, score_change, etc)
├── content_type FK(ContentType) -- What was affected
├── object_id INT
├── old_values JSON
├── new_values JSON
├── timestamp TIMESTAMP
├── ip_address VARCHAR
├── reason TEXT-- 6 critical indexes
idx_submission_user_problem -- Check for duplicates
idx_submission_contest_user -- Contest leaderboard
idx_submission_state_correct -- Find pending grades
idx_submission_submitted_at -- Time range queries
idx_problem_category_difficulty -- Browse problems
idx_audit_log_user_timestamp -- Admin audit queries1. Student views problem (ProblemVersion latest)
2. Student submits answer
├── Create Submission record (IMMUTABLE)
├── Compute answer_hash
├── Set state='submitted'
3. Auto-grader evaluates
├── Compare normalized answers
├── Create ScoringRecord (score=100 or 0)
├── Set state='graded', is_correct=True/False
4. Student sees result
5. (Optional) Student appeals
├── Create Appeal record
├── Teacher reviews
├── Create new ScoringRecord with new score
├── Update Submission.score (denormalized)
├── Close Appeal
1. Admin creates Contest
2. Admin adds problems (via ContestProblem)
3. Students register (ContestRegistration)
4. Contest starts (start_time reached)
├── Students can now submit
├── Leaderboard snapshots computed every 5 min
5. Contest runs
6. Freeze time reached (10 min before end)
├── Leaderboard stops updating
├── Submissions still allowed
7. Contest ends (end_time reached)
├── No more submissions allowed
├── Final leaderboard snapshot created (is_final=True)
├── Results certified
8. Students can appeal (within appeal window)
9. Admin publishes results
1. Admin views Problem v1 (IMMUTABLE, never change)
2. Admin clicks "Edit"
3. Django creates NEW ProblemVersion
├── problem_id stays same (GEOM-2025-001)
├── version_number increments (1 → 2)
├── All fields copied from v1
├── Admin modifies specific fields
4. Admin saves
├── Create ProblemVersion v2 (NEW record)
├── Create ProblemEditHistory (v1 → v2)
├── Create AuditLog
5. Result:
├── All old submissions still reference v1 (unaffected)
├── New submissions reference v2
├── Can compare old vs new for disputes
Rejected: Mutable Problem
# BAD: Admin edits answer on Wednesday
problem.answer = "new_answer"
problem.save()
# Now 50 submissions with old answer are marked wrong!Chosen: Immutable Versions
# GOOD: New version created
ProblemVersion(problem_id="GEO-001", version_number=2)
# Old submissions still reference version_number=1
# No retroactive unfairnessRejected: Mutable Score
submission.score = 100
submission.save()
# Who changed it? Why? What was before? Unknown.Chosen: Immutable Audit Trail
ScoringRecord.create(
submission=s,
old_score=0,
new_score=100,
change_reason='appeal_granted',
changed_by=teacher,
notes='Calculation error in problem'
)
# Perfect audit trail for disputes, compliance, abuse detectionRejected: Live Leaderboard
SELECT user, COUNT(*) FROM submissions
WHERE is_correct=True GROUP BY user
-- Problem 1: Query joins 100k rows (slow, timeouts)
-- Problem 2: Results change every second (students see live updates, timing attacks)
-- Problem 3: No historical records (can't verify "was I 5th at the time?")Chosen: Batch Snapshots
# Every 5 minutes during contest
snapshot = LeaderboardSnapshot.create(
contest=contest,
leaderboard_data=[{rank, user, score}, ...] # JSON
is_final=False
)
# Benefits:
# - Fast reads (one JSON field per contest)
# - No timing attacks (stale data is OK)
# - Historical records (can audit "was 5th at 2pm?")
# - Frozen before end (final result certified)Rejected: Hard Delete
problem.delete()
# CASCADE deletes all submissions!
# Foreign key constraint failures
# Audit trail disappears
# Can't recover if mistakeChosen: Soft Delete
problem.is_active = False
# Queries filter is_active=True by default
# Foreign keys intact
# Full audit trail
# Can restore if needed
# Compliant with GDPR "right to be forgotten" (flag, not erase)| Operation | Time | Notes |
|---|---|---|
| Login | <50ms | Session, Redis backed |
| Browse problems | <100ms | Cached, indexed by category+difficulty |
| View problem | <50ms | Immutable, cacheable |
| Submit answer | <100ms | Validation + hash computation |
| Auto-grade | <10ms | Hash comparison |
| Load leaderboard | <50ms | JSON field, single query |
| View profile | <100ms | User + stats joined |
- 100,000 active students
- 1,000,000 submissions
- 100 concurrent contests
- 10,000 problems
Indexes used: 6 strategic indexes cover 95%+ of queries
# Run tests
python manage.py test
# Run with coverage
coverage run --source='.' manage.py test
coverage report
# Specific app
python manage.py test accounts.tests.UserTest- Unit: Models, utility functions
- Integration: View workflows, permissions
- Load: Concurrent submissions, leaderboard updates
- Security: SQL injection, XSS, CSRF
Django==4.2.27
psycopg2==2.9.10 # PostgreSQL
celery==5.3.0 # Background tasks
redis==5.0.0 # Caching
gunicorn==21.0.0 # Production server
python-decouple==3.8 # Environment config
django-crispy-forms==2.0 # Forms
pillow==10.0.0 # Image processing
requests==2.31.0 # HTTP client
See requirements.txt for full list.
- DEVELOPMENT_ROADMAP.md - Week-by-week development plan
- DEPLOYMENT_GUIDE.md - Production setup
- API_DESIGN.md - Future REST API (coming soon)
- SECURITY.md - Security audit checklist
| Pitfall | How We Avoid It |
|---|---|
| N+1 queries | select_related(), prefetch_related() in all views |
| Mutable data corruption | Immutable models with soft deletes |
| Audit trail loss | AuditLog on every change |
| Timing attacks | Frozen leaderboards, batch snapshots |
| Category shifting | Category snapshot on User |
| Problem changes | ProblemVersion immutable |
| Score disputes | ScoringRecord immutable trail |
| Admin abuse | Role-based permissions (future) |
| Database corruption | Constraints at DB level |
| Performance collapse | Indexes on all filter/join columns |
- Notify users immediately
- Force password reset
- Check AuditLog for unauthorized access
- Suspend affected accounts
- Switch to read-only mode
- Failover to replica
- Restore from backup
- Audit affected data
- Stop writes
- Check AuditLog for what changed
- Revert via ScoringRecord history
- Investigate root cause
MIT License - See LICENSE file
This is a production system - all changes require:
- Tests (>85% coverage)
- Code review
- Security audit (if touching permissions/auth)
- Database migration (if schema changes)
- Issues: GitHub Issues
- Documentation: See /docs
- Email: support@mathoj.example.com
- Clone and set up locally
- Read DEVELOPMENT_ROADMAP.md
- Run tests:
python manage.py test - Deploy using DEPLOYMENT_GUIDE.md
- Monitor with logs and audit trail
- Scale following performance guide
Built with brutally pragmatic architecture for million-submission scale, zero tolerance for data loss, and uncompromising fairness.