Compare commits
17 Commits
d342f888a9
...
darkmode
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dc45b9a9b | |||
| 57a7b0e68f | |||
| 68b7b81741 | |||
| e97dde65fb | |||
| 6079813e2c | |||
| 04dc638cf0 | |||
| 38c3256f19 | |||
| 1374cb9cb1 | |||
| c1ef310f6a | |||
| 525297f19b | |||
| 89ea87951e | |||
| c7c1b8ecd6 | |||
| 0bffdf612c | |||
| a03f9ddb14 | |||
| 3f1f709f30 | |||
| 1ac0a13fc3 | |||
| 5fe140c4f9 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
instance
|
instance
|
||||||
.venv
|
.venv
|
||||||
*.sqlite3
|
venv
|
||||||
|
*.sqlite3
|
||||||
|
|||||||
89
app.py
89
app.py
@@ -1,89 +0,0 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for
|
|
||||||
from models import db, Problem, Solution
|
|
||||||
from utils import run_code_against_tests
|
|
||||||
from leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
from problem_loader import load_problems_from_json, schedule_problem_reload
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3'
|
|
||||||
db.init_app(app)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def setup():
|
|
||||||
db.create_all()
|
|
||||||
create_leaderboard_table() # Ensure leaderboard table exists
|
|
||||||
# Load problems from JSON at startup
|
|
||||||
json_path = os.path.join(os.path.dirname(__file__), 'problems.json')
|
|
||||||
load_problems_from_json(json_path)
|
|
||||||
# Schedule reload every 10 hours
|
|
||||||
schedule_problem_reload(app, json_path, interval_hours=10)
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
problems = Problem.query.all()
|
|
||||||
leaderboard = get_leaderboard()
|
|
||||||
# Map problem_id to title for leaderboard display
|
|
||||||
problem_titles = {p.id: p.title for p in problems}
|
|
||||||
return render_template('index.html', problems=problems, leaderboard=leaderboard, problem_titles=problem_titles)
|
|
||||||
|
|
||||||
@app.route('/problem/new', methods=['GET', 'POST'])
|
|
||||||
def new_problem():
|
|
||||||
if request.method == 'POST':
|
|
||||||
title = request.form['title']
|
|
||||||
description = request.form['description']
|
|
||||||
test_code = request.form['test_code']
|
|
||||||
problem = Problem(title=title, description=description, test_code=test_code)
|
|
||||||
db.session.add(problem)
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(url_for('index'))
|
|
||||||
return render_template('new_problem.html')
|
|
||||||
|
|
||||||
@app.route('/problem/<int:problem_id>', methods=['GET', 'POST'])
|
|
||||||
def view_problem(problem_id):
|
|
||||||
problem = Problem.query.get_or_404(problem_id)
|
|
||||||
result = None
|
|
||||||
if request.method == 'POST':
|
|
||||||
user_code = request.form['user_code']
|
|
||||||
username = request.form.get('username', '').strip() or 'Anonymous'
|
|
||||||
import tracemalloc
|
|
||||||
tracemalloc.start()
|
|
||||||
run_result = run_code_against_tests(user_code, problem.test_code)
|
|
||||||
current, peak = tracemalloc.get_traced_memory()
|
|
||||||
tracemalloc.stop()
|
|
||||||
memory_used = peak // 1024 # in KB
|
|
||||||
# Try to get the last line number executed (even for successful runs)
|
|
||||||
line_number = None
|
|
||||||
import ast
|
|
||||||
try:
|
|
||||||
tree = ast.parse(user_code)
|
|
||||||
# Get the last line number in the user's code
|
|
||||||
if hasattr(tree, 'body') and tree.body:
|
|
||||||
last_node = tree.body[-1]
|
|
||||||
line_number = getattr(last_node, 'lineno', None)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# If there was an error, try to get the error line number from the traceback
|
|
||||||
if run_result['error']:
|
|
||||||
tb = run_result['error']
|
|
||||||
import traceback
|
|
||||||
try:
|
|
||||||
tb_lines = traceback.extract_tb(traceback.TracebackException.from_string(tb).stack)
|
|
||||||
if tb_lines:
|
|
||||||
line_number = tb_lines[-1].lineno
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
log_leaderboard(username, problem.id, run_result['runtime'], memory_used, line_number)
|
|
||||||
solution = Solution(problem_id=problem.id, user_code=user_code, passed=run_result['passed'], output=run_result['output'])
|
|
||||||
db.session.add(solution)
|
|
||||||
db.session.commit()
|
|
||||||
result = run_result
|
|
||||||
|
|
||||||
return render_template('problem.html', problem=problem, result=result)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(debug=True)
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from models import db, Problem
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
def load_problems_from_json(json_path):
|
|
||||||
if not os.path.exists(json_path):
|
|
||||||
print(f"Problem JSON file not found: {json_path}")
|
|
||||||
return
|
|
||||||
with open(json_path, 'r') as f:
|
|
||||||
problems = json.load(f)
|
|
||||||
for p in problems:
|
|
||||||
# Check if problem already exists by title
|
|
||||||
existing = Problem.query.filter_by(title=p['title']).first()
|
|
||||||
# Load test code from solution file if provided
|
|
||||||
test_code = ''
|
|
||||||
if 'solution' in p and os.path.exists(p['solution']):
|
|
||||||
with open(p['solution'], 'r') as sf:
|
|
||||||
test_code = sf.read()
|
|
||||||
if existing:
|
|
||||||
existing.description = p['description']
|
|
||||||
existing.test_code = test_code
|
|
||||||
else:
|
|
||||||
new_problem = Problem(title=p['title'], description=p['description'], test_code=test_code)
|
|
||||||
db.session.add(new_problem)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
def schedule_problem_reload(app, json_path, interval_hours=10):
|
|
||||||
def reload_loop():
|
|
||||||
while True:
|
|
||||||
with app.app_context():
|
|
||||||
load_problems_from_json(json_path)
|
|
||||||
time.sleep(interval_hours * 3600)
|
|
||||||
t = threading.Thread(target=reload_loop, daemon=True)
|
|
||||||
t.start()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"title": "Reversed String",
|
|
||||||
"description": "Reverse a String with a Function (revstring); the function is supposed to take the string as an argument and is supposed to return the reversed string and print it.",
|
|
||||||
"solution": "problems/solution_reversed_string.py"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Sort List",
|
|
||||||
"description": "Sort a List with a Function (sortlist); the function is supposed to take the list as an argument and is supposed to return the sorted list and print it.",
|
|
||||||
"solution": "problems/sortlist.py"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
#<!-- The Function the User needs to write -->
|
|
||||||
def revstring(x):
|
|
||||||
return x[::-1]
|
|
||||||
|
|
||||||
#<!-- This Test, test if the function works -->
|
|
||||||
class TestSolution(unittest.TestCase):
|
|
||||||
def test_simple(self):
|
|
||||||
x="";
|
|
||||||
self.assertEqual(revstring(x), x[::-1])
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
class TestSolution(unittest.TestCase):
|
|
||||||
def test_sort(self):
|
|
||||||
# define x as a empty array.
|
|
||||||
# this will be used for the functiun ; a empty var does not work.
|
|
||||||
self.x = []
|
|
||||||
self.assertEqual(sortlist(self.x), sorted(self.x)) ## sort
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
55
readme.md
55
readme.md
@@ -4,22 +4,59 @@
|
|||||||
|
|
||||||
but more lightweight
|
but more lightweight
|
||||||
|
|
||||||
|
run the bash script to start the server.
|
||||||
|
|
||||||
if you want to contribute write tests like this:
|
if you want to contribute write tests like this:
|
||||||
|
|
||||||
|
### FileStructure:
|
||||||
|
|
||||||
|
In /problems/ create a folder named after the problem.
|
||||||
|
|
||||||
|
In this folder create ```manifest.json, test.py, description.md```
|
||||||
|
|
||||||
|
**Manifest.JSON needs to exsist and _needs_ to look like this:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Title of the Problem",
|
||||||
|
"description": "Write a very short description here",
|
||||||
|
"description_md": "problems/problempath/description.md",
|
||||||
|
"difficulty": "easy || medium || hard",
|
||||||
|
"test_code": "problems/problempath/test.py"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
I do know it might be a bit tedious but this is required and its the easiest way.
|
||||||
|
|
||||||
|
#### After you've decided on how you would name / write your Test write it like this:
|
||||||
|
|
||||||
|
- It is important to note that you _CAN_ write the Code the User is expected to write firstly. **BUT** after writing the UnitTest and it passing, comment out the written code.
|
||||||
|
|
||||||
|
It is supposed to look something like this (/sortlist/):
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
"""
|
||||||
|
@TESTSAMPLE.PY / NAME THIS "test.py" in your actual project
|
||||||
|
"""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
#<!-- The Function the User needs to write -->
|
" )) First Point from the List "
|
||||||
def revstring(x):
|
# def sortlist(lst = [4,3,2,1]) -> list:
|
||||||
return x[::-1]
|
# return sorted(lst)
|
||||||
|
|
||||||
#<!-- This Test, test if the function works -->
|
")) This is a 'easy' Test, if you want you can write more defined ones."
|
||||||
class TestSolution(unittest.TestCase):
|
class TestSolution(unittest.TestCase):
|
||||||
def test_simple(self):
|
def test_sort(self):
|
||||||
# !! This needs to be dynamic ; if the user enters some shit then it is supposed to work too
|
self.x = []
|
||||||
x="";
|
self.assertEqual(sortlist(self.x), sorted(self.x)) # pyright: ignore[reportUndefinedVariable] <- This is only here so that pyright doesnt complain ; NOT NECCESARY!
|
||||||
self.assertEqual(revstring(x), x[::-1])
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
```
|
```
|
||||||
|
#### Writing the description:
|
||||||
|
|
||||||
|
**Please** by _God_ write simple and easy to understand terms. If you write like Einstein noone is going to understand you.
|
||||||
|
|
||||||
|
- Syntax:
|
||||||
|
- Normal Markdown.
|
||||||
|
- Start with "##" instead of "#" ; "##" looks better
|
||||||
|
- Use CrossLinks ( something like [W3](https://www.w3schools.com/), or the [PyDocs](https://docs.python.org/3/))
|
||||||
|
- Good Formatting is always appreciated
|
||||||
|
|||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Flask>=3.0
|
||||||
|
Flask-SQLAlchemy>=3.1
|
||||||
|
Markdown>=3.6
|
||||||
|
MarkupSafe>=2.1
|
||||||
|
watchdog>=4.0
|
||||||
17
run.bash
Normal file
17
run.bash
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
u!/bin/bash
|
||||||
|
|
||||||
|
set -e # exit if any command fails
|
||||||
|
|
||||||
|
# Ensure QPP/database directory exists
|
||||||
|
mkdir -p src/database
|
||||||
|
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export FLASK_APP=src.app
|
||||||
|
export FLASK_ENV=production
|
||||||
|
|
||||||
|
flask run --host=0.0.0.0 --port=5000
|
||||||
1
run.bat
Normal file
1
run.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
python -m flask --app .\src\app.py run --host=0.0.0.0 --port=5000
|
||||||
136
src/app.py
Normal file
136
src/app.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
from markupsafe import Markup
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, send_from_directory
|
||||||
|
import markdown as md
|
||||||
|
import ast
|
||||||
|
from src.models import db, Problem, Solution
|
||||||
|
from src.utils import run_code_against_tests
|
||||||
|
from src.leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
## from problem_loader import load_problems_from_json, schedule_problem_reload
|
||||||
|
from src.problem_scanner import start_problem_scanner
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).parent
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{BASE_DIR / 'database' / 'db.sqlite3'}"
|
||||||
|
|
||||||
|
print(f">>>>>>>>>>>>>>>>>>>>< Using database URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def setup():
|
||||||
|
db.create_all()
|
||||||
|
create_leaderboard_table() # Ensure leaderboard table exists
|
||||||
|
# Problems are now loaded from manifests by the background scanner. No need to load problems.json.
|
||||||
|
|
||||||
|
# Start the background thread to scan problems
|
||||||
|
start_problem_scanner()
|
||||||
|
|
||||||
|
@app.route("/script.js")
|
||||||
|
def script():
|
||||||
|
return send_from_directory("templates", "script.js")
|
||||||
|
|
||||||
|
@app.route('/favicon.ico')
|
||||||
|
def favicon():
|
||||||
|
return send_from_directory("templates", "favicon.ico")
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
#<!-- The query was fucked up so it fetched the fucking testcode -->
|
||||||
|
c.execute('SELECT folder, description, test_code, difficulty FROM problems')
|
||||||
|
problems = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
# Get leaderboard entries
|
||||||
|
leaderboard = get_leaderboard()
|
||||||
|
# Map folder to title for display
|
||||||
|
problem_titles = {folder: folder.replace('_', ' ').title() for folder, _, _, _ in problems}
|
||||||
|
return render_template('index.html', problems=problems, leaderboard=leaderboard, problem_titles=problem_titles)
|
||||||
|
|
||||||
|
@app.route('/problem/new', methods=['GET', 'POST'])
|
||||||
|
def new_problem():
|
||||||
|
if request.method == 'POST':
|
||||||
|
title = request.form['title']
|
||||||
|
description = request.form['description']
|
||||||
|
test_code = request.form['test_code']
|
||||||
|
problem = Problem(title=title, description=description, test_code=test_code)
|
||||||
|
db.session.add(problem)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
return render_template('new_problem.html')
|
||||||
|
|
||||||
|
@app.route('/problem/<folder>', methods=['GET', 'POST'])
|
||||||
|
def view_problem(folder):
|
||||||
|
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT folder, description,test_code , difficulty FROM problems WHERE folder = ?', (folder,))
|
||||||
|
row = c.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return 'Problem not found', 404
|
||||||
|
|
||||||
|
problem = {
|
||||||
|
'folder': row[0],
|
||||||
|
'description': row[1],
|
||||||
|
'difficulty': row[3], # now correct
|
||||||
|
'test_code': row[2], # now correct
|
||||||
|
}
|
||||||
|
|
||||||
|
result = None
|
||||||
|
if request.method == 'POST':
|
||||||
|
user_code = request.form['user_code']
|
||||||
|
username = request.form.get('username', '').strip() or 'Anonymous'
|
||||||
|
import tracemalloc
|
||||||
|
tracemalloc.start()
|
||||||
|
run_result = run_code_against_tests(user_code, problem['test_code'])
|
||||||
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
|
tracemalloc.stop()
|
||||||
|
memory_used = peak // 1024 # in KB
|
||||||
|
|
||||||
|
# Try to get the last line number executed (even for successful runs)
|
||||||
|
line_number = None
|
||||||
|
try:
|
||||||
|
tree = ast.parse(user_code)
|
||||||
|
# Find the highest line number in the AST (for multi-function/user code)
|
||||||
|
def get_max_lineno(node):
|
||||||
|
max_lineno = getattr(node, 'lineno', 0)
|
||||||
|
for child in ast.iter_child_nodes(node):
|
||||||
|
max_lineno = max(max_lineno, get_max_lineno(child))
|
||||||
|
return max_lineno
|
||||||
|
line_number = get_max_lineno(tree)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If there was an error, try to get the error line number from the traceback
|
||||||
|
if run_result['error']:
|
||||||
|
tb = run_result['error']
|
||||||
|
import traceback
|
||||||
|
try:
|
||||||
|
tb_lines = traceback.extract_tb(traceback.TracebackException.from_string(tb).stack)
|
||||||
|
if tb_lines:
|
||||||
|
line_number = tb_lines[-1].lineno
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ONLY log to leaderboard if the solution passed all tests
|
||||||
|
if run_result['passed']:
|
||||||
|
log_leaderboard(username, problem['folder'], run_result['runtime'], memory_used, line_number)
|
||||||
|
|
||||||
|
result = run_result
|
||||||
|
return render_template('problem.html', problem=problem, result=result)
|
||||||
|
|
||||||
|
@app.template_filter('markdown')
|
||||||
|
def markdown_filter(text):
|
||||||
|
return Markup(md.markdown(text or '', extensions=['extra', 'sane_lists']))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
||||||
@@ -2,12 +2,14 @@ from flask_sqlalchemy import SQLAlchemy
|
|||||||
from flask import g
|
from flask import g
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = getattr(g, '_database', None)
|
if 'db' not in g:
|
||||||
if db is None:
|
db_path = Path(__file__).parent / 'database' / 'db.sqlite3'
|
||||||
db = g._database = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'db.sqlite3'))
|
db_path.parent.mkdir(exist_ok=True) # Ensure /database folder exists
|
||||||
return db
|
g.db = sqlite3.connect(db_path)
|
||||||
|
return g.db
|
||||||
|
|
||||||
def create_leaderboard_table():
|
def create_leaderboard_table():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
384
src/problem_scanner.py
Normal file
384
src/problem_scanner.py
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import random
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
WATCHDOG_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
WATCHDOG_AVAILABLE = False
|
||||||
|
|
||||||
|
PROBLEMS_DIR = Path(__file__).parent / 'problems'
|
||||||
|
DB_PATH = Path(__file__).parent / 'database/problems.sqlite3'
|
||||||
|
|
||||||
|
class ProblemScannerThread(threading.Thread):
|
||||||
|
def __init__(self, scan_interval=2):
|
||||||
|
super().__init__(daemon=True)
|
||||||
|
self.scan_interval = scan_interval
|
||||||
|
self.last_state = {}
|
||||||
|
self.observer = None
|
||||||
|
|
||||||
|
def create_table(self, conn):
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('PRAGMA journal_mode=WAL;')
|
||||||
|
c.execute('''CREATE TABLE IF NOT EXISTS problems (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
folder TEXT,
|
||||||
|
description TEXT,
|
||||||
|
difficulty TEXT,
|
||||||
|
test_code TEXT
|
||||||
|
)''')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def scan(self):
|
||||||
|
problems = []
|
||||||
|
if not PROBLEMS_DIR.exists():
|
||||||
|
print(f"Problems directory does not exist: {PROBLEMS_DIR}")
|
||||||
|
return problems
|
||||||
|
|
||||||
|
for folder in PROBLEMS_DIR.iterdir():
|
||||||
|
if folder.is_dir():
|
||||||
|
# Dynamically find manifest file (manifest.json or manifets.json)
|
||||||
|
manifest_path = None
|
||||||
|
for candidate in ["manifest.json", "manifets.json"]:
|
||||||
|
candidate_path = folder / candidate
|
||||||
|
if candidate_path.exists():
|
||||||
|
manifest_path = candidate_path
|
||||||
|
break
|
||||||
|
|
||||||
|
desc_path = folder / 'description.md'
|
||||||
|
test_path = folder / 'test.py'
|
||||||
|
|
||||||
|
# Check if required files exist
|
||||||
|
if manifest_path and desc_path.exists() and test_path.exists():
|
||||||
|
try:
|
||||||
|
with open(desc_path, 'r', encoding='utf-8') as f:
|
||||||
|
description = f.read()
|
||||||
|
with open(test_path, 'r', encoding='utf-8') as f:
|
||||||
|
test_code = f.read()
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
|
manifest = json.load(f)
|
||||||
|
|
||||||
|
difficulty = manifest.get('difficulty', 'unknown')
|
||||||
|
|
||||||
|
problems.append({
|
||||||
|
'folder': folder.name,
|
||||||
|
'description': description,
|
||||||
|
'test_code': test_code,
|
||||||
|
'difficulty': difficulty
|
||||||
|
})
|
||||||
|
print(f"Found problem: {folder.name} ; Difficulty: {difficulty}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading problem files for {folder.name}: {e}")
|
||||||
|
else:
|
||||||
|
missing_files = []
|
||||||
|
if not manifest_path:
|
||||||
|
missing_files.append("manifest.json/manifets.json")
|
||||||
|
if not desc_path.exists():
|
||||||
|
missing_files.append("description.md")
|
||||||
|
if not test_path.exists():
|
||||||
|
missing_files.append("test.py")
|
||||||
|
print(f"Skipping {folder.name}: missing {', '.join(missing_files)}")
|
||||||
|
|
||||||
|
print(f"Total problems found: {len(problems)}")
|
||||||
|
return problems
|
||||||
|
|
||||||
|
def update_db(self, problems, retries=5):
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH, timeout=5)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('PRAGMA journal_mode=WAL;')
|
||||||
|
|
||||||
|
# Clear existing problems
|
||||||
|
c.execute('DELETE FROM problems')
|
||||||
|
|
||||||
|
# Insert new problems
|
||||||
|
for p in problems:
|
||||||
|
c.execute('''INSERT INTO problems
|
||||||
|
(folder, description, difficulty, test_code)
|
||||||
|
VALUES (?, ?, ?, ?)''',
|
||||||
|
(p['folder'], p['description'], p['difficulty'], p['test_code']))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"Updated database with {len(problems)} problems")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if 'locked' in str(e).lower():
|
||||||
|
wait_time = 0.2 + random.random() * 0.3
|
||||||
|
print(f"Database locked, retrying in {wait_time:.2f}s (attempt {attempt + 1})")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
else:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error updating database: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
print('Failed to update problems DB after several retries due to lock.')
|
||||||
|
|
||||||
|
def rescan_and_update(self):
|
||||||
|
print("Scanning for problems...")
|
||||||
|
problems = self.scan()
|
||||||
|
self.update_db(problems)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
print("Starting problem scanner...")
|
||||||
|
|
||||||
|
# Initial scan and table creation
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
self.create_table(conn)
|
||||||
|
conn.close()
|
||||||
|
print("Database initialized")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to initialize database: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Initial scan
|
||||||
|
self.rescan_and_update()
|
||||||
|
|
||||||
|
if WATCHDOG_AVAILABLE:
|
||||||
|
print("Using watchdog for file monitoring")
|
||||||
|
|
||||||
|
class Handler(FileSystemEventHandler):
|
||||||
|
def __init__(self, scanner):
|
||||||
|
self.scanner = scanner
|
||||||
|
self.last_event_time = 0
|
||||||
|
|
||||||
|
def on_any_event(self, event):
|
||||||
|
# Debounce events to avoid too many rescans
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_event_time > 1: # Wait at least 1 second between rescans
|
||||||
|
self.last_event_time = now
|
||||||
|
print(f"File system event: {event.event_type} - {event.src_path}")
|
||||||
|
self.scanner.rescan_and_update()
|
||||||
|
|
||||||
|
event_handler = Handler(self)
|
||||||
|
self.observer = Observer()
|
||||||
|
self.observer.schedule(event_handler, str(PROBLEMS_DIR), recursive=True)
|
||||||
|
self.observer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopping problem scanner...")
|
||||||
|
finally:
|
||||||
|
self.observer.stop()
|
||||||
|
self.observer.join()
|
||||||
|
else:
|
||||||
|
print(f"Watchdog not available, using polling every {self.scan_interval}s")
|
||||||
|
# Fallback: poll every scan_interval seconds
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(self.scan_interval)
|
||||||
|
self.rescan_and_update()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopping problem scanner...")
|
||||||
|
|
||||||
|
def start_problem_scanner():
|
||||||
|
scanner = ProblemScannerThread()
|
||||||
|
scanner.start()
|
||||||
|
return scanner
|
||||||
|
|
||||||
|
# Flask model loading functions
|
||||||
|
def load_problems_from_json(json_path):
|
||||||
|
"""Load problems from JSON file into Flask database"""
|
||||||
|
if not os.path.exists(json_path):
|
||||||
|
print(f"Problem JSON file not found: {json_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r', encoding='utf-8') as f:
|
||||||
|
problems = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading JSON file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# This assumes you have imported the necessary Flask/SQLAlchemy components
|
||||||
|
try:
|
||||||
|
from models import db, Problem
|
||||||
|
|
||||||
|
for p in problems:
|
||||||
|
# Check if problem already exists by title
|
||||||
|
existing = Problem.query.filter_by(title=p['title']).first()
|
||||||
|
|
||||||
|
# Load test code from solution file if provided
|
||||||
|
test_code = ''
|
||||||
|
if 'solution' in p and os.path.exists(p['solution']):
|
||||||
|
try:
|
||||||
|
with open(p['solution'], 'r', encoding='utf-8') as sf:
|
||||||
|
test_code = sf.read()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading solution file for {p['title']}: {e}")
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
existing.description = p['description']
|
||||||
|
existing.test_code = test_code
|
||||||
|
print(f"Updated problem: {p['title']}")
|
||||||
|
else:
|
||||||
|
new_problem = Problem(title=p['title'], description=p['description'], test_code=test_code)
|
||||||
|
db.session.add(new_problem)
|
||||||
|
print(f"Added new problem: {p['title']}")
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("Successfully updated problems from JSON")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("Flask models not available - skipping JSON load")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading problems from JSON: {e}")
|
||||||
|
|
||||||
|
def schedule_problem_reload(app, json_path, interval_hours=10):
|
||||||
|
"""Schedule periodic reloading of problems from JSON"""
|
||||||
|
def reload_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
with app.app_context():
|
||||||
|
load_problems_from_json(json_path)
|
||||||
|
time.sleep(interval_hours * 3600)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in problem reload loop: {e}")
|
||||||
|
time.sleep(60) # Wait 1 minute before retrying
|
||||||
|
|
||||||
|
t = threading.Thread(target=reload_loop, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def run_code_against_tests(user_code, test_code, timeout=10):
|
||||||
|
"""
|
||||||
|
Execute user code against test code with proper error handling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_code: The user's solution code
|
||||||
|
test_code: The test code to validate the solution
|
||||||
|
timeout: Maximum execution time in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with passed, output, runtime, and error fields
|
||||||
|
"""
|
||||||
|
if not user_code or not user_code.strip():
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'output': '',
|
||||||
|
'runtime': 0,
|
||||||
|
'error': 'No code provided'
|
||||||
|
}
|
||||||
|
|
||||||
|
if not test_code or not test_code.strip():
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'output': '',
|
||||||
|
'runtime': 0,
|
||||||
|
'error': 'No test code available'
|
||||||
|
}
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
output = ''
|
||||||
|
error = None
|
||||||
|
passed = False
|
||||||
|
temp_file = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if unittest is used in test_code
|
||||||
|
if 'unittest' in test_code:
|
||||||
|
# Create temporary file with user code + test code
|
||||||
|
with tempfile.NamedTemporaryFile('w+', suffix='.py', delete=False, encoding='utf-8') as f:
|
||||||
|
# Combine user code and test code
|
||||||
|
combined_code = f"{user_code}\n\n{test_code}"
|
||||||
|
f.write(combined_code)
|
||||||
|
f.flush()
|
||||||
|
temp_file = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run the file as a subprocess with timeout
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, temp_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
output = proc.stdout
|
||||||
|
if proc.stderr:
|
||||||
|
output += f"\nSTDERR:\n{proc.stderr}"
|
||||||
|
|
||||||
|
passed = proc.returncode == 0
|
||||||
|
if not passed:
|
||||||
|
error = f"Tests failed. Return code: {proc.returncode}\n{output}"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
passed = False
|
||||||
|
error = f"Code execution timed out after {timeout} seconds"
|
||||||
|
output = "Execution timed out"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Direct execution approach for simple assert-based tests
|
||||||
|
local_ns = {}
|
||||||
|
|
||||||
|
# Capture stdout
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
captured_output = io.StringIO()
|
||||||
|
sys.stdout = captured_output
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute user code first
|
||||||
|
exec(user_code, {}, local_ns)
|
||||||
|
|
||||||
|
# Execute test code in the same namespace
|
||||||
|
exec(test_code, local_ns, local_ns)
|
||||||
|
|
||||||
|
# If we get here without exceptions, tests passed
|
||||||
|
passed = True
|
||||||
|
|
||||||
|
except AssertionError as e:
|
||||||
|
passed = False
|
||||||
|
error = f"Assertion failed: {str(e)}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
passed = False
|
||||||
|
error = f"Runtime error: {traceback.format_exc()}"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
output = captured_output.getvalue()
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
passed = False
|
||||||
|
error = f"Execution error: {traceback.format_exc()}"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up temporary file
|
||||||
|
if temp_file and os.path.exists(temp_file):
|
||||||
|
try:
|
||||||
|
os.unlink(temp_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not delete temp file {temp_file}: {e}")
|
||||||
|
|
||||||
|
runtime = time.perf_counter() - start_time
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'passed': passed,
|
||||||
|
'output': output.strip() if output else '',
|
||||||
|
'runtime': runtime,
|
||||||
|
'error': error if not passed else None
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Test execution result: passed={passed}, runtime={runtime:.3f}s")
|
||||||
|
if error:
|
||||||
|
print(f"Error: {error}")
|
||||||
|
|
||||||
|
return result
|
||||||
60
src/problems/Palindrome/description.md
Normal file
60
src/problems/Palindrome/description.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
## Problem: Check if a String is a Palindrome
|
||||||
|
|
||||||
|
Given a string `s`, determine whether it reads the same forward and backward.
|
||||||
|
Return `True` if it is a palindrome, otherwise return `False`.
|
||||||
|
|
||||||
|
A **palindrome** is a sequence of characters that is identical when reversed.
|
||||||
|
Comparison is **case-sensitive** and should consider all characters, including spaces and punctuation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 1
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```
|
||||||
|
s = "racecar"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
```
|
||||||
|
True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Explanation:**
|
||||||
|
Reversing `"racecar"` results in `"racecar"`, which is the same as the original string.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 2
|
||||||
|
|
||||||
|
**Input:**
|
||||||
|
|
||||||
|
```
|
||||||
|
s = "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
|
||||||
|
```
|
||||||
|
False
|
||||||
|
```
|
||||||
|
|
||||||
|
**Explanation:**
|
||||||
|
Reversing `"hello"` results in `"olleh"`, which is different from the original string.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
|
||||||
|
* `0 <= len(s) <= 10^5`
|
||||||
|
* `s` may contain letters, digits, symbols, and spaces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Function Signature (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def palindrome(s: str) -> bool:
|
||||||
|
```
|
||||||
7
src/problems/Palindrome/manifest.json
Normal file
7
src/problems/Palindrome/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Palindrome",
|
||||||
|
"description": "Find out wether or not a String is a Palindrome",
|
||||||
|
"description_md": "problems/Palindrome/description.md",
|
||||||
|
"test_code": "problems/Palindrome/test.py",
|
||||||
|
"difficulty": "medium"
|
||||||
|
}
|
||||||
32
src/problems/Palindrome/test.py
Normal file
32
src/problems/Palindrome/test.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
#<!-- User expected Function -->
|
||||||
|
## def palindrome(s:str) -> bool:
|
||||||
|
## return s == s[::-1]
|
||||||
|
|
||||||
|
class TestSolution(unittest.TestCase):
|
||||||
|
def test_palindrome(self):
|
||||||
|
test_cases = [
|
||||||
|
("racecar", True), # Simple palindrome
|
||||||
|
("hello", False), # Not a palindrome
|
||||||
|
("", True), # Empty string
|
||||||
|
("a", True), # Single character
|
||||||
|
("madam", True), # Palindrome word
|
||||||
|
("Madam", False), # Case-sensitive check
|
||||||
|
("12321", True), # Numeric string palindrome
|
||||||
|
("123456", False), # Numeric string non-palindrome
|
||||||
|
]
|
||||||
|
print("\nFUNCTION OUTPUT TEST RESULTS")
|
||||||
|
|
||||||
|
for input_val, expected in test_cases:
|
||||||
|
try:
|
||||||
|
actual = palindrome(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||||
|
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||||
|
print(f"{status} | Input: '{input_val}' -> Got: {actual} | Expected: {expected}")
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ ERROR | Input: '{input_val}' -> Exception: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
44
src/problems/ReversedList/description.md
Normal file
44
src/problems/ReversedList/description.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
## Reverse a List
|
||||||
|
|
||||||
|
Write a function called `reverse_list` that takes a list as input and returns the list in reverse order.
|
||||||
|
You are **not allowed** to just use Python’s built-in `.reverse()` method or slicing (`[::-1]`) — try to reverse it manually for practice.
|
||||||
|
|
||||||
|
### Function Signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def reverse_list(lst):
|
||||||
|
# your code here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* The function should return a new list with the elements in reversed order.
|
||||||
|
* The input list can contain:
|
||||||
|
|
||||||
|
* Numbers
|
||||||
|
* Strings
|
||||||
|
* Booleans
|
||||||
|
* A mix of different types
|
||||||
|
* Your function will be tested with:
|
||||||
|
|
||||||
|
* A small list (e.g., `[1, 2, 3]` → `[3, 2, 1]`)
|
||||||
|
* A longer list (e.g., `[1, 2, 3, 4]` → `[4, 3, 2, 1]`)
|
||||||
|
* An empty list (should return an empty list)
|
||||||
|
* A single-element list (should return the same list)
|
||||||
|
* A mixed-type list (e.g., `[1, 'a', True]` → `[True, 'a', 1]`)
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
reverse_list([1, 2, 3])
|
||||||
|
# Output: [3, 2, 1]
|
||||||
|
|
||||||
|
reverse_list([])
|
||||||
|
# Output: []
|
||||||
|
|
||||||
|
reverse_list([5])
|
||||||
|
# Output: [5]
|
||||||
|
|
||||||
|
reverse_list([1, 'a', True])
|
||||||
|
# Output: [True, 'a', 1]
|
||||||
|
```
|
||||||
7
src/problems/ReversedList/manifest.json
Normal file
7
src/problems/ReversedList/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Reversed List",
|
||||||
|
"description": "Given a list, return a new list with the elements in reverse order.",
|
||||||
|
"description_md": "problems/reversedlist/description.md",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"test_code": "problems/reversedlist/test.py"
|
||||||
|
}
|
||||||
27
src/problems/ReversedList/test.py
Normal file
27
src/problems/ReversedList/test.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
#def reverse_list(lst : list) -> list:
|
||||||
|
#return lst[::-1]
|
||||||
|
|
||||||
|
class TestSolution(unittest.TestCase):
|
||||||
|
def test_simple(self):
|
||||||
|
test_cases = [
|
||||||
|
([1, 2, 3], [3, 2, 1]), # Simple case
|
||||||
|
([1, 2, 3, 4], [4, 3, 2, 1]), # Longer list
|
||||||
|
([], []), # Empty list
|
||||||
|
([5], [5]), # Single element list
|
||||||
|
([1, 'a', True], [True, 'a', 1]) # Mixed types
|
||||||
|
]
|
||||||
|
print("\n FUNCTION OUTPUT TEST RESULTS")
|
||||||
|
|
||||||
|
for input_val , expected in test_cases:
|
||||||
|
try:
|
||||||
|
actual = reverse_list(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||||
|
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||||
|
print(f"{status} | Input: {input_val} -> Got: {actual} | Expected: {expected}")
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ ERROR | Input: {input_val} -> Exception: {e}")
|
||||||
|
raise
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
40
src/problems/fibonacisequence/description.md
Normal file
40
src/problems/fibonacisequence/description.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
## Fibonacci Number
|
||||||
|
|
||||||
|
Write a function called `fibonacci` that takes a non-negative integer `n` as input and returns the **n-th Fibonacci number**.
|
||||||
|
|
||||||
|
The Fibonacci sequence is defined as:
|
||||||
|
|
||||||
|
* `F(0) = 0`
|
||||||
|
* `F(1) = 1`
|
||||||
|
* `F(n) = F(n-1) + F(n-2)` for `n > 1`
|
||||||
|
|
||||||
|
### Function Signature:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def fibonacci(n):
|
||||||
|
# return your solution
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
|
||||||
|
* The function should return the `n`-th number in the Fibonacci sequence.
|
||||||
|
* If `n` is less than `0`, print `"Incorrect input"`.
|
||||||
|
* Your function will be tested with:
|
||||||
|
|
||||||
|
* Base cases (`n = 0` and `n = 1`)
|
||||||
|
* Small values of `n`
|
||||||
|
* Larger values of `n` (e.g., 9)
|
||||||
|
* Multiple test cases in sequence
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
fibonacci(0) # returns 0
|
||||||
|
fibonacci(1) # returns 1
|
||||||
|
fibonacci(2) # returns 1
|
||||||
|
fibonacci(3) # returns 2
|
||||||
|
fibonacci(5) # returns 5
|
||||||
|
fibonacci(9) # returns 34
|
||||||
|
```
|
||||||
|
|
||||||
|
You can copy this into your problem’s solution description.
|
||||||
7
src/problems/fibonacisequence/manifest.json
Normal file
7
src/problems/fibonacisequence/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Fibonacci Sequence",
|
||||||
|
"description": "Calculate the n-th Fibonacci number using a function. The Fibonacci sequence is defined as follows: F(0) = 0, F(1) = 1, and F(n) = F(n-1) + F(n-2) for n > 1.",
|
||||||
|
"description_md": "problems/fibonacisequence/description.md",
|
||||||
|
"difficulty": "medium",
|
||||||
|
"test_code": "problems/fibonacisequence/test.py"
|
||||||
|
}
|
||||||
52
src/problems/fibonacisequence/test.py
Normal file
52
src/problems/fibonacisequence/test.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestSolution(unittest.TestCase):
|
||||||
|
def test_simple(self):
|
||||||
|
test_cases = [
|
||||||
|
(0, 0), # Base case: n = 0
|
||||||
|
(1, 1), # Base case: n = 1
|
||||||
|
(2, 1), # Fibonacci(2) = 1
|
||||||
|
(3, 2), # Fibonacci(3) = 2
|
||||||
|
(5, 5), # Fibonacci(5) = 5
|
||||||
|
(9, 34), # Fibonacci(9) = 34
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n=== Function Output Test Results ===")
|
||||||
|
for input_val, expected in test_cases:
|
||||||
|
try:
|
||||||
|
actual = fibonacci(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||||
|
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||||
|
print(f"{status} | Input: {input_val} -> Got: {actual} | Expected: {expected}")
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ ERROR | Input: {input_val} -> Exception: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def fibonacci(n):
|
||||||
|
a = 0
|
||||||
|
b = 1
|
||||||
|
|
||||||
|
# Check if n is less than 0
|
||||||
|
if n < 0:
|
||||||
|
print("Incorrect input")
|
||||||
|
|
||||||
|
# Check if n is equal to 0
|
||||||
|
elif n == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Check if n is equal to 1
|
||||||
|
elif n == 1:
|
||||||
|
return b
|
||||||
|
else:
|
||||||
|
for i in range(1, n):
|
||||||
|
c = a + b
|
||||||
|
a = b
|
||||||
|
b = c
|
||||||
|
return b
|
||||||
|
|
||||||
|
print(fibonacci(9))
|
||||||
|
"""
|
||||||
58
src/problems/regex-phone/description.md
Normal file
58
src/problems/regex-phone/description.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Phone Number Regular Expression Validation
|
||||||
|
|
||||||
|
You are asked to write a function that checks if a given string is a valid phone number.
|
||||||
|
|
||||||
|
A valid phone number must follow this format:
|
||||||
|
|
||||||
|
```python
|
||||||
|
123-456-7890
|
||||||
|
```
|
||||||
|
|
||||||
|
* It contains **3 digits**, followed by a **dash (-)**
|
||||||
|
* Then another **3 digits**, followed by a **dash (-)**
|
||||||
|
* Then exactly **4 digits**
|
||||||
|
|
||||||
|
If the string matches this exact format, return **True**. Otherwise, return **False**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 1
|
||||||
|
|
||||||
|
```python
|
||||||
|
Input: "123-456-7890"
|
||||||
|
Output: True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2
|
||||||
|
|
||||||
|
```python
|
||||||
|
Input: "1234567890"
|
||||||
|
Output: False
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3
|
||||||
|
|
||||||
|
```python
|
||||||
|
Input: "abc-def-ghij"
|
||||||
|
Output: False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Function Signature
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
def is_valid_phone_number(phone_number: str) -> bool:
|
||||||
|
return bool("Your Solution Here!")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hint 🔑
|
||||||
|
|
||||||
|
* Use the **`re`** (regular expression) library.
|
||||||
|
* `\d` means “a digit” in regex.
|
||||||
|
* You will need exactly **3 digits**, then a dash, then **3 digits**, another dash, then **4 digits**.
|
||||||
|
* Anchors `^` (start of string) and `$` (end of string) can help ensure the whole string matches.
|
||||||
7
src/problems/regex-phone/manifest.json
Normal file
7
src/problems/regex-phone/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Regex Phonenumber",
|
||||||
|
"description": "A regex problem to match phone numbers in various formats.",
|
||||||
|
"description_md": "problems/regex-phone/description.md",
|
||||||
|
"difficulty": "hard",
|
||||||
|
"test_code": "problems/regex-phone/test.py"
|
||||||
|
}
|
||||||
33
src/problems/regex-phone/test.py
Normal file
33
src/problems/regex-phone/test.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
## def is_valid_phone_number(phone_number : str):
|
||||||
|
## return bool(re.search(r"^(\d{3}-){2}\d{4}$", phone_number))
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestPhoneNumberRegex(unittest.TestCase):
|
||||||
|
def test_if_valid(self):
|
||||||
|
test_cases = [
|
||||||
|
("123-456-7890", True), # Valid format
|
||||||
|
("111-222-3333", True), # Another valid format
|
||||||
|
("abc-def-ghij", False), # Letters instead of digits
|
||||||
|
("1234567890", False), # Missing dashes
|
||||||
|
("123-45-67890", False), # Wrong grouping
|
||||||
|
("12-3456-7890", False), # Wrong grouping again
|
||||||
|
("", False), # Empty string
|
||||||
|
]
|
||||||
|
print("\nPHONE NUMBER VALIDATION TEST RESULTS")
|
||||||
|
|
||||||
|
for phone, expected in test_cases:
|
||||||
|
try:
|
||||||
|
actual = is_valid_phone_number(phone) # pyright: ignore[reportUndefinedVariable]
|
||||||
|
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||||
|
print(f"{status} | Input: '{phone}' -> Got: {actual} | Expected: {expected}")
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ ERROR | Input: '{phone}' -> Exception: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
29
src/problems/reversedstring/description.md
Normal file
29
src/problems/reversedstring/description.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## Reversed String
|
||||||
|
|
||||||
|
Write a function called ```revstring``` that takes a string as input and returns the reversed string.
|
||||||
|
|
||||||
|
### Function Signature:
|
||||||
|
```python
|
||||||
|
def revstring(x).
|
||||||
|
# return your solution
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- The function should return the input string reversed
|
||||||
|
- Your function will be tested with various cases, including:
|
||||||
|
- An empty string
|
||||||
|
- A single character
|
||||||
|
- A palindrome ("racecar")
|
||||||
|
- A string of numbers ("12345")
|
||||||
|
- Special characters
|
||||||
|
- A normal string ( "Hello World" )
|
||||||
|
|
||||||
|
#### Example:
|
||||||
|
```python
|
||||||
|
revstring("Hello World") # returns "dlroW olleH"
|
||||||
|
revstring("") # returns ""
|
||||||
|
revstring("racecar") # returns "racecar"
|
||||||
|
revstring("12345") # returns "54321"
|
||||||
|
revstring("!@# $%") # returns "%$ #@!"
|
||||||
|
```
|
||||||
|
You can copy this into your problems solution
|
||||||
7
src/problems/reversedstring/manifest.json
Normal file
7
src/problems/reversedstring/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title":"Reversed String",
|
||||||
|
"description":"Reverse a String using a Function ; Try to write as little code as possible",
|
||||||
|
"description_md":"problems/reversedstring/description.md",
|
||||||
|
"difficulty":"easy",
|
||||||
|
"test_code":"problems/reversedstring/test.py"
|
||||||
|
}
|
||||||
26
src/problems/reversedstring/test.py
Normal file
26
src/problems/reversedstring/test.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestSolution(unittest.TestCase):
|
||||||
|
def test_simple(self):
|
||||||
|
test_cases = [
|
||||||
|
("Hello World", "dlroW olleH"),
|
||||||
|
("", ""),
|
||||||
|
("a", "a"),
|
||||||
|
("racecar", "racecar"),
|
||||||
|
("12345", "54321"),
|
||||||
|
("!@# $%", "%$ #@!")
|
||||||
|
]
|
||||||
|
|
||||||
|
print("\n=== Function Output Test Results ===")
|
||||||
|
for input_val, expected in test_cases:
|
||||||
|
try:
|
||||||
|
actual = revstring(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||||
|
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||||
|
print(f"{status} | Input: '{input_val}' -> Got: '{actual}' | Expected: '{expected}'")
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ ERROR | Input: '{input_val}' -> Exception: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
1
src/problems/sortlist/description.md
Normal file
1
src/problems/sortlist/description.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
this is a easy sorting problem **it is solvable in less than 2 seconds**
|
||||||
7
src/problems/sortlist/manifets.json
Normal file
7
src/problems/sortlist/manifets.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "Sort List",
|
||||||
|
"description": "Sort a List with a Function (sortlist); the function is supposed to take the list as an argument and is supposed to return the sorted list and print it.",
|
||||||
|
"description_md": "problems/sortlist/description.md",
|
||||||
|
"difficulty": "easy",
|
||||||
|
"test_code": "problems/sortlist/test.py"
|
||||||
|
}
|
||||||
17
src/problems/sortlist/test.py
Normal file
17
src/problems/sortlist/test.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
# This is the function the user is expected to write.
|
||||||
|
# Its a really simple one, the user can choose not to type tho.
|
||||||
|
|
||||||
|
#def sortlist(lst = [4,3,2,1]) -> list:
|
||||||
|
#return sorted(lst)
|
||||||
|
|
||||||
|
class TestSolution(unittest.TestCase):
|
||||||
|
def test_sort(self):
|
||||||
|
# define x as a empty array.
|
||||||
|
# this will be used for the functiun ; a empty var does not work.
|
||||||
|
self.x = []
|
||||||
|
self.assertEqual(sortlist(self.x), sorted(self.x)) # pyright: ignore[reportUndefinedVariable]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
BIN
src/static/favicon.ico
Normal file
BIN
src/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
326
src/static/index.css
Normal file
326
src/static/index.css
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #f6f8fb;
|
||||||
|
--card: #fff;
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--hover: #f3f4f6;
|
||||||
|
--shadow: 0 4px 12px rgba(16, 24, 40, 0.06);
|
||||||
|
--radius: 8px;
|
||||||
|
--mono: "JetBrains Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode variables */
|
||||||
|
html.dark {
|
||||||
|
--bg: #0f172a;
|
||||||
|
--card: #1e293b;
|
||||||
|
--text: #f1f5f9;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--accent: #3b82f6;
|
||||||
|
--border: #334155;
|
||||||
|
--hover: #334155;
|
||||||
|
--shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Inter, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
color 0.3s ease;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
header p {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.dark-mode-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.dark-mode-toggle:hover {
|
||||||
|
background: var(--hover);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
html.dark .dark-mode-icon::before {
|
||||||
|
content: "☀︎️";
|
||||||
|
}
|
||||||
|
html:not(.dark) .dark-mode-icon::before {
|
||||||
|
content: "⏾";
|
||||||
|
}
|
||||||
|
.dark-mode-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.dark-mode-icon::before {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.content.single-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
/* Search/filter controls */
|
||||||
|
.search-controls {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.filter-select {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.filter-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
/* Problems list */
|
||||||
|
.problems-list .problem-item {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.problem-item:hover {
|
||||||
|
background: var(--hover);
|
||||||
|
}
|
||||||
|
.problem-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.problem-item a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
.problem-item a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
/* Difficulty badge */
|
||||||
|
.difficulty {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: white;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.difficulty[data-difficulty="easy"] {
|
||||||
|
background-color: #4caf50; /* Green */
|
||||||
|
}
|
||||||
|
.difficulty[data-difficulty="medium"] {
|
||||||
|
background-color: #ffc107; /* Amber */
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.difficulty[data-difficulty="hard"] {
|
||||||
|
background-color: #f44336; /* Red */
|
||||||
|
}
|
||||||
|
/* Leaderboard */
|
||||||
|
.leaderboard-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.leaderboard-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.leaderboard-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.leaderboard-table th,
|
||||||
|
.leaderboard-table td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.leaderboard-table th {
|
||||||
|
background: var(--hover);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.leaderboard-table tr:hover {
|
||||||
|
background: var(--hover);
|
||||||
|
}
|
||||||
|
/* Sort indicators */
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
.sortable::after {
|
||||||
|
content: "↕";
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.sort-asc::after {
|
||||||
|
content: "↑";
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.sort-desc::after {
|
||||||
|
content: "↓";
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
/* Toggle button */
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: rgba(37, 99, 235, 0.08);
|
||||||
|
}
|
||||||
|
.btn.active {
|
||||||
|
background: rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.leaderboard-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaderboard horizontal collapse */
|
||||||
|
#leaderboardSection {
|
||||||
|
transition:
|
||||||
|
max-width 0.35s ease,
|
||||||
|
opacity 0.25s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboardSection.hidden {
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#leaderboardSection.visible {
|
||||||
|
max-width: 100%; /* take full available space in grid column */
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#rankingExplanation {
|
||||||
|
transition: all 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination Controls */
|
||||||
|
.pagination-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.pagination-btn {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.pagination-btn:hover:not(:disabled) {
|
||||||
|
background: var(--hover);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.pagination-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.pagination-info {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide pagination when not needed */
|
||||||
|
.pagination-controls.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
296
src/static/problem.css
Normal file
296
src/static/problem.css
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #f9f9f9;
|
||||||
|
--card: #fff;
|
||||||
|
--text: #333;
|
||||||
|
--muted: #666;
|
||||||
|
--accent: #007bff;
|
||||||
|
--accent-hover: #0069d9;
|
||||||
|
--border: #eaeaea;
|
||||||
|
--hover: #f8f9fa;
|
||||||
|
--code-bg: #f6f8fa;
|
||||||
|
--editor-border: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
--bg: #0f172a;
|
||||||
|
--card: #1e293b;
|
||||||
|
--text: #f1f5f9;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--accent: #3b82f6;
|
||||||
|
--accent-hover: #2563eb;
|
||||||
|
--border: #334155;
|
||||||
|
--hover: #334155;
|
||||||
|
--code-bg: #1e293b;
|
||||||
|
--editor-border: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh; /* allow content to grow */
|
||||||
|
overflow-y: auto; /* allow vertical scroll */
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap; /* wrap on small screens */
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-panel {
|
||||||
|
flex: 1 1 400px; /* grow/shrink with base 400px */
|
||||||
|
min-width: 300px;
|
||||||
|
background: var(--card);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
max-height: 100vh;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1 1 400px;
|
||||||
|
min-width: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--card);
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: hidden; /* internal scroll handling */
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-right: 15px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode-toggle:hover {
|
||||||
|
background: var(--hover);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .dark-mode-icon::before {
|
||||||
|
content: "☀";
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) .dark-mode-icon::before {
|
||||||
|
content: "⏾";
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode-icon::before {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-desc {
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 15px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-desc pre {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-desc code {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
padding: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions button {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions button:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid var(--editor-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-panel {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--hover);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
min-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 30vh;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-panel h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-panel pre {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--text);
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-container {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
.problem-panel,
|
||||||
|
.editor-container {
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
min-width: auto;
|
||||||
|
max-height: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
#editor {
|
||||||
|
min-height: 400px;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
.result-panel {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
403
src/static/script.js
Normal file
403
src/static/script.js
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
// Dark mode functionality
|
||||||
|
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
// Load saved dark mode preference
|
||||||
|
const savedDarkMode = localStorage.getItem("darkMode");
|
||||||
|
if (
|
||||||
|
savedDarkMode === "true" ||
|
||||||
|
(savedDarkMode === null &&
|
||||||
|
// detect if the user already has a dark mode enabled in the system settings ( works for all systems )
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
) {
|
||||||
|
html.classList.add("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
darkModeToggle?.addEventListener("click", () => {
|
||||||
|
html.classList.toggle("dark");
|
||||||
|
localStorage.setItem("darkMode", html.classList.contains("dark"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Problem search and pagination
|
||||||
|
const problemSearch = document.getElementById("problemSearch");
|
||||||
|
const problemsContainer = document.getElementById("problemsContainer");
|
||||||
|
const problemsPagination = document.getElementById("problemsPagination");
|
||||||
|
const problemsPrevBtn = document.getElementById("problemsPrevBtn");
|
||||||
|
const problemsNextBtn = document.getElementById("problemsNextBtn");
|
||||||
|
const problemsPaginationInfo = document.getElementById(
|
||||||
|
"problemsPaginationInfo",
|
||||||
|
);
|
||||||
|
|
||||||
|
let allProblemItems = [];
|
||||||
|
let filteredProblemItems = [];
|
||||||
|
let currentPage = 1;
|
||||||
|
const itemsPerPage = 5;
|
||||||
|
|
||||||
|
// Initialize problem items
|
||||||
|
function initializeProblemItems() {
|
||||||
|
allProblemItems = Array.from(
|
||||||
|
problemsContainer?.querySelectorAll(".problem-item") || [],
|
||||||
|
);
|
||||||
|
filteredProblemItems = [...allProblemItems];
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagination() {
|
||||||
|
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
|
||||||
|
// Hide all items first
|
||||||
|
allProblemItems.forEach((item) => {
|
||||||
|
item.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show current page items
|
||||||
|
filteredProblemItems.slice(startIndex, endIndex).forEach((item) => {
|
||||||
|
item.style.display = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update pagination controls
|
||||||
|
if (problemsPrevBtn) problemsPrevBtn.disabled = currentPage <= 1;
|
||||||
|
if (problemsNextBtn) problemsNextBtn.disabled = currentPage >= totalPages;
|
||||||
|
if (problemsPaginationInfo) {
|
||||||
|
problemsPaginationInfo.textContent =
|
||||||
|
totalPages > 0
|
||||||
|
? `Page ${currentPage} of ${totalPages}`
|
||||||
|
: "No problems found";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide pagination if not needed
|
||||||
|
if (problemsPagination) {
|
||||||
|
problemsPagination.classList.toggle("hidden", totalPages <= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterProblems() {
|
||||||
|
const term = problemSearch?.value.toLowerCase().trim() || "";
|
||||||
|
filteredProblemItems = allProblemItems.filter((item) => {
|
||||||
|
const name = item.dataset.name?.toLowerCase() || "";
|
||||||
|
const desc = item.dataset.desc?.toLowerCase() || "";
|
||||||
|
return !term || name.includes(term) || desc.includes(term);
|
||||||
|
});
|
||||||
|
currentPage = 1;
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners for pagination
|
||||||
|
problemsPrevBtn?.addEventListener("click", () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
currentPage--;
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
problemsNextBtn?.addEventListener("click", () => {
|
||||||
|
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage);
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
currentPage++;
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
problemSearch?.addEventListener("input", filterProblems);
|
||||||
|
|
||||||
|
// Initialize problems pagination
|
||||||
|
if (problemsContainer) {
|
||||||
|
initializeProblemItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaderboard functionality
|
||||||
|
const problemFilter = document.getElementById("problemFilter");
|
||||||
|
const runtimeFilter = document.getElementById("runtimeFilter");
|
||||||
|
const leaderboardBody = document.getElementById("leaderboardBody");
|
||||||
|
const sortableHeaders = document.querySelectorAll(".sortable");
|
||||||
|
|
||||||
|
let currentSort = { column: "rank", direction: "asc" };
|
||||||
|
let allRows = [];
|
||||||
|
|
||||||
|
// Initialize rows array
|
||||||
|
function initializeRows() {
|
||||||
|
allRows = Array.from(leaderboardBody.querySelectorAll("tr")).map((row) => {
|
||||||
|
return {
|
||||||
|
element: row,
|
||||||
|
user: row.dataset.user || "",
|
||||||
|
problem: row.dataset.problem || "",
|
||||||
|
runtime: parseFloat(row.dataset.runtime) || 0,
|
||||||
|
memory: parseFloat(row.dataset.memory) || 0,
|
||||||
|
timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(),
|
||||||
|
language: row.dataset.language || "",
|
||||||
|
originalIndex: Array.from(leaderboardBody.children).indexOf(row),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRankClasses() {
|
||||||
|
const visibleRows = allRows.filter(
|
||||||
|
(row) => row.element.style.display !== "none",
|
||||||
|
);
|
||||||
|
visibleRows.forEach((rowData, index) => {
|
||||||
|
const rank = index + 1;
|
||||||
|
const row = rowData.element;
|
||||||
|
|
||||||
|
// Update rank cell
|
||||||
|
const rankCell = row.cells[0];
|
||||||
|
if (rankCell) rankCell.textContent = rank;
|
||||||
|
|
||||||
|
// Update rank classes
|
||||||
|
row.className = row.className.replace(/\brank-\d+\b/g, "");
|
||||||
|
if (rank === 1) row.classList.add("rank-1");
|
||||||
|
else if (rank <= 3) row.classList.add("rank-top3");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOverallRanking() {
|
||||||
|
const visibleRows = allRows.filter(
|
||||||
|
(row) => row.element.style.display !== "none",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (visibleRows.length === 0) return;
|
||||||
|
|
||||||
|
// Group submissions by problem to find the best performance for each
|
||||||
|
const problemBests = {};
|
||||||
|
|
||||||
|
visibleRows.forEach((rowData) => {
|
||||||
|
const problem = rowData.problem;
|
||||||
|
if (!problemBests[problem]) {
|
||||||
|
problemBests[problem] = {
|
||||||
|
bestRuntime: Infinity,
|
||||||
|
bestMemory: Infinity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
problemBests[problem].bestRuntime = Math.min(
|
||||||
|
problemBests[problem].bestRuntime,
|
||||||
|
rowData.runtime,
|
||||||
|
);
|
||||||
|
problemBests[problem].bestMemory = Math.min(
|
||||||
|
problemBests[problem].bestMemory,
|
||||||
|
rowData.memory,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate normalized scores for each submission
|
||||||
|
visibleRows.forEach((rowData) => {
|
||||||
|
const problemBest = problemBests[rowData.problem];
|
||||||
|
|
||||||
|
// Prevent division by zero
|
||||||
|
const runtimeScore =
|
||||||
|
problemBest.bestRuntime > 0
|
||||||
|
? rowData.runtime / problemBest.bestRuntime
|
||||||
|
: 1;
|
||||||
|
const memoryScore =
|
||||||
|
problemBest.bestMemory > 0
|
||||||
|
? rowData.memory / problemBest.bestMemory
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
// Weighted overall score (70% runtime, 30% memory)
|
||||||
|
rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by overall score (lower is better), then by timestamp (earlier is better for ties)
|
||||||
|
visibleRows.sort((a, b) => {
|
||||||
|
const scoreDiff = a.overallScore - b.overallScore;
|
||||||
|
if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; // Use small epsilon for float comparison
|
||||||
|
|
||||||
|
// If scores are essentially equal, prefer earlier submission
|
||||||
|
return a.timestamp - b.timestamp;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reorder DOM elements and update ranks
|
||||||
|
visibleRows.forEach((rowData, index) => {
|
||||||
|
leaderboardBody.appendChild(rowData.element);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRankClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterLeaderboard() {
|
||||||
|
const problemTerm = (problemFilter?.value || "").toLowerCase().trim();
|
||||||
|
const runtimeType = runtimeFilter?.value || "all";
|
||||||
|
|
||||||
|
// Reset all rows to visible first
|
||||||
|
allRows.forEach((rowData) => {
|
||||||
|
rowData.element.style.display = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply problem filter
|
||||||
|
if (problemTerm) {
|
||||||
|
allRows.forEach((rowData) => {
|
||||||
|
const problemMatch = rowData.problem
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(problemTerm);
|
||||||
|
if (!problemMatch) {
|
||||||
|
rowData.element.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply runtime filter (best/worst per user per problem)
|
||||||
|
if (runtimeType === "best" || runtimeType === "worst") {
|
||||||
|
const userProblemGroups = {};
|
||||||
|
|
||||||
|
// Group by user + problem combination
|
||||||
|
allRows.forEach((rowData) => {
|
||||||
|
if (rowData.element.style.display === "none") return;
|
||||||
|
|
||||||
|
const key = `${rowData.user}::${rowData.problem}`;
|
||||||
|
if (!userProblemGroups[key]) {
|
||||||
|
userProblemGroups[key] = [];
|
||||||
|
}
|
||||||
|
userProblemGroups[key].push(rowData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide all except best/worst for each user-problem combination
|
||||||
|
Object.values(userProblemGroups).forEach((group) => {
|
||||||
|
if (group.length <= 1) return;
|
||||||
|
|
||||||
|
// Sort by runtime
|
||||||
|
group.sort((a, b) => a.runtime - b.runtime);
|
||||||
|
|
||||||
|
const keepIndex = runtimeType === "best" ? 0 : group.length - 1;
|
||||||
|
group.forEach((rowData, index) => {
|
||||||
|
if (index !== keepIndex) {
|
||||||
|
rowData.element.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateOverallRanking();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellValue(rowData, column) {
|
||||||
|
switch (column) {
|
||||||
|
case "rank":
|
||||||
|
return parseInt(rowData.element.cells[0]?.textContent) || 0;
|
||||||
|
case "user":
|
||||||
|
return rowData.user.toLowerCase();
|
||||||
|
case "problem":
|
||||||
|
return rowData.problem.toLowerCase();
|
||||||
|
case "runtime":
|
||||||
|
return rowData.runtime;
|
||||||
|
case "memory":
|
||||||
|
return rowData.memory;
|
||||||
|
case "timestamp":
|
||||||
|
return rowData.timestamp;
|
||||||
|
case "language":
|
||||||
|
return rowData.language.toLowerCase();
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortLeaderboard(column, direction) {
|
||||||
|
if (column === "rank") {
|
||||||
|
calculateOverallRanking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleRows = allRows.filter(
|
||||||
|
(row) => row.element.style.display !== "none",
|
||||||
|
);
|
||||||
|
|
||||||
|
visibleRows.sort((a, b) => {
|
||||||
|
const valueA = getCellValue(a, column);
|
||||||
|
const valueB = getCellValue(b, column);
|
||||||
|
|
||||||
|
let comparison = 0;
|
||||||
|
if (typeof valueA === "number" && typeof valueB === "number") {
|
||||||
|
comparison = valueA - valueB;
|
||||||
|
} else {
|
||||||
|
comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return direction === "asc" ? comparison : -comparison;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reorder DOM elements
|
||||||
|
visibleRows.forEach((rowData) => {
|
||||||
|
leaderboardBody.appendChild(rowData.element);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRankClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners for sorting
|
||||||
|
sortableHeaders.forEach((header) => {
|
||||||
|
header.addEventListener("click", () => {
|
||||||
|
const column = header.dataset.sort;
|
||||||
|
if (!column) return;
|
||||||
|
|
||||||
|
// Remove sorting classes from all headers
|
||||||
|
sortableHeaders.forEach((h) =>
|
||||||
|
h.classList.remove("sort-asc", "sort-desc"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle sort direction
|
||||||
|
if (currentSort.column === column) {
|
||||||
|
currentSort.direction =
|
||||||
|
currentSort.direction === "asc" ? "desc" : "asc";
|
||||||
|
} else {
|
||||||
|
currentSort.column = column;
|
||||||
|
currentSort.direction = "asc";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sorting class to current header
|
||||||
|
header.classList.add(`sort-${currentSort.direction}`);
|
||||||
|
|
||||||
|
sortLeaderboard(column, currentSort.direction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter event listeners
|
||||||
|
problemFilter?.addEventListener("input", filterLeaderboard);
|
||||||
|
runtimeFilter?.addEventListener("change", filterLeaderboard);
|
||||||
|
|
||||||
|
// Rank info popout
|
||||||
|
const rankInfoBtn = document.getElementById("rankInfoBtn");
|
||||||
|
const rankingExplanation = document.getElementById("rankingExplanation");
|
||||||
|
|
||||||
|
rankInfoBtn?.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
rankingExplanation?.classList.toggle("active");
|
||||||
|
rankInfoBtn?.classList.toggle("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close ranking explanation when clicking outside
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (
|
||||||
|
rankingExplanation?.classList.contains("active") &&
|
||||||
|
!rankingExplanation.contains(e.target) &&
|
||||||
|
!rankInfoBtn?.contains(e.target)
|
||||||
|
) {
|
||||||
|
rankingExplanation.classList.remove("active");
|
||||||
|
rankInfoBtn?.classList.remove("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize everything
|
||||||
|
if (leaderboardBody && leaderboardBody.children.length > 0) {
|
||||||
|
initializeRows();
|
||||||
|
calculateOverallRanking();
|
||||||
|
|
||||||
|
// Set initial sort indicator
|
||||||
|
const defaultHeader = document.querySelector('[data-sort="rank"]');
|
||||||
|
if (defaultHeader) {
|
||||||
|
defaultHeader.classList.add("sort-asc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply dark mode to dynamically created elements
|
||||||
|
function applyDarkModeToElements() {
|
||||||
|
const isDark = html.classList.contains("dark");
|
||||||
|
// Any additional dark mode styling for dynamically created elements can go here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for dark mode changes
|
||||||
|
new MutationObserver(applyDarkModeToElements).observe(html, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"],
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,260 +1,317 @@
|
|||||||
/* Reset and base styles */
|
:root {
|
||||||
* {
|
--bg: #f8f9fa;
|
||||||
margin: 0;
|
--card: #fff;
|
||||||
padding: 0;
|
--text: #333;
|
||||||
box-sizing: border-box;
|
--heading: #2c3e50;
|
||||||
}
|
--heading-secondary: #34495e;
|
||||||
|
--accent: #3498db;
|
||||||
body {
|
--accent-hover: #2980b9;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
--success: #27ae60;
|
||||||
line-height: 1.6;
|
--success-hover: #229954;
|
||||||
color: #333;
|
--error: #e74c3c;
|
||||||
background-color: #f8f9fa;
|
--muted: #6c757d;
|
||||||
padding: 20px;
|
--muted-hover: #5a6268;
|
||||||
max-width: 1200px;
|
--border: #ddd;
|
||||||
margin: 0 auto;
|
--code-bg: #f4f4f4;
|
||||||
}
|
--success-bg: #d4edda;
|
||||||
|
--success-text: #155724;
|
||||||
/* Main heading */
|
--error-bg: #f8d7da;
|
||||||
h1 {
|
--error-text: #721c24;
|
||||||
color: #2c3e50;
|
--hover-bg: #e3f2fd;
|
||||||
margin-bottom: -10px;
|
--shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
padding-bottom: 3px;
|
}
|
||||||
border-bottom: 3px solid #3498db;
|
|
||||||
font-size: 2.2em;
|
html.dark {
|
||||||
}
|
--bg: #0f172a;
|
||||||
|
--card: #1e293b;
|
||||||
h2 {
|
--text: #f1f5f9;
|
||||||
color: #34495e;
|
--heading: #3b82f6;
|
||||||
margin: 30px 0 20px 0;
|
--heading-secondary: #94a3b8;
|
||||||
font-size: 1.5em;
|
--accent: #3b82f6;
|
||||||
}
|
--accent-hover: #2563eb;
|
||||||
|
--success: #22c55e;
|
||||||
h3 {
|
--success-hover: #16a34a;
|
||||||
color: #34495e;
|
--error: #ef4444;
|
||||||
margin: 25px 0 15px 0;
|
--muted: #64748b;
|
||||||
font-size: 1.3em;
|
--muted-hover: #475569;
|
||||||
}
|
--border: #334155;
|
||||||
|
--code-bg: #1e293b;
|
||||||
/* Links and buttons */
|
--success-bg: #065f46;
|
||||||
a {
|
--success-text: #d1fae5;
|
||||||
color: #3498db;
|
--error-bg: #7f1d1d;
|
||||||
text-decoration: none;
|
--error-text: #fecaca;
|
||||||
padding: 8px 16px;
|
--hover-bg: #1e40af;
|
||||||
border-radius: 5px;
|
--shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
transition: background-color 0.3s ease;
|
}
|
||||||
}
|
|
||||||
|
/* Reset and base styles */
|
||||||
a:hover {
|
* {
|
||||||
background-color: #e3f2fd;
|
margin: 0;
|
||||||
text-decoration: none;
|
padding: 0;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
/* Primary action link (Submit New Problem) */
|
|
||||||
a[href="/problem/new"] {
|
body {
|
||||||
background-color: #3498db;
|
font-family:
|
||||||
color: white;
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
font-weight: 600;
|
line-height: 1.6;
|
||||||
margin-bottom: 30px;
|
color: var(--text);
|
||||||
display: inline-block;
|
background-color: var(--bg);
|
||||||
padding: 12px 24px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
max-width: 1200px;
|
||||||
}
|
margin: 0 auto;
|
||||||
|
transition:
|
||||||
a[href="/problem/new"]:hover {
|
background-color 0.3s ease,
|
||||||
background-color: #2980b9;
|
color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Problem list */
|
/* Main heading */
|
||||||
ul {
|
h1 {
|
||||||
list-style: none;
|
color: var(--heading);
|
||||||
background: white;
|
margin-bottom: -10px;
|
||||||
border-radius: 8px;
|
padding-bottom: 3px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
border-bottom: 3px solid var(--accent);
|
||||||
padding: 25px;
|
font-size: 2.2em;
|
||||||
margin: 20px 0;
|
}
|
||||||
}
|
|
||||||
|
h2 {
|
||||||
li {
|
color: var(--heading-secondary);
|
||||||
padding: 15px 0;
|
margin: 30px 0 20px 0;
|
||||||
border-bottom: 1px solid #eee;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
li:last-child {
|
h3 {
|
||||||
border-bottom: none;
|
color: var(--heading-secondary);
|
||||||
}
|
margin: 25px 0 15px 0;
|
||||||
|
font-size: 1.3em;
|
||||||
li a {
|
}
|
||||||
display: block;
|
|
||||||
padding: 12px 20px;
|
/* Links and buttons */
|
||||||
margin: -12px -20px;
|
a {
|
||||||
border-radius: 6px;
|
color: var(--accent);
|
||||||
font-size: 1.1em;
|
text-decoration: none;
|
||||||
}
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
li a:hover {
|
transition: background-color 0.3s ease;
|
||||||
background-color: #f8f9fa;
|
}
|
||||||
transform: translateX(5px);
|
|
||||||
transition: all 0.2s ease;
|
a:hover {
|
||||||
}
|
background-color: var(--hover-bg);
|
||||||
|
text-decoration: none;
|
||||||
/* Problem page specific styles */
|
}
|
||||||
.problem-header {
|
|
||||||
display: flex;
|
/* Primary action link (Submit New Problem) */
|
||||||
align-items: center;
|
a[href="/problem/new"] {
|
||||||
margin-bottom: 30px;
|
background-color: var(--accent);
|
||||||
gap: 20px;
|
color: white;
|
||||||
}
|
font-weight: 600;
|
||||||
|
margin-bottom: 30px;
|
||||||
.back-btn {
|
display: inline-block;
|
||||||
background-color: #95a5a6;
|
padding: 12px 24px;
|
||||||
color: white;
|
border-radius: 8px;
|
||||||
border: none;
|
}
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 6px;
|
a[href="/problem/new"]:hover {
|
||||||
cursor: pointer;
|
background-color: var(--accent-hover);
|
||||||
font-size: 14px;
|
}
|
||||||
font-weight: 500;
|
|
||||||
transition: background-color 0.3s ease;
|
/* Problem list */
|
||||||
}
|
ul {
|
||||||
|
list-style: none;
|
||||||
.back-btn:hover {
|
background: var(--card);
|
||||||
background-color: #7f8c8d;
|
border-radius: 8px;
|
||||||
}
|
box-shadow: var(--shadow);
|
||||||
|
padding: 25px;
|
||||||
.problem-desc {
|
margin: 20px 0;
|
||||||
background: white;
|
transition: background-color 0.3s ease;
|
||||||
padding: 30px;
|
}
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
li {
|
||||||
margin-bottom: 30px;
|
padding: 15px 0;
|
||||||
font-size: 1.1em;
|
border-bottom: 1px solid var(--border);
|
||||||
line-height: 1.7;
|
}
|
||||||
}
|
|
||||||
|
li:last-child {
|
||||||
/* Editor section */
|
border-bottom: none;
|
||||||
.editor-section {
|
}
|
||||||
background: white;
|
|
||||||
padding: 30px;
|
li a {
|
||||||
border-radius: 8px;
|
display: block;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
padding: 12px 20px;
|
||||||
margin-bottom: 30px;
|
margin: -12px -20px;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
font-size: 1.1em;
|
||||||
#editor {
|
}
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: 8px;
|
li a:hover {
|
||||||
margin: 20px 0;
|
background-color: var(--hover-bg);
|
||||||
height: 400px;
|
transform: translateX(5px);
|
||||||
overflow: hidden;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-actions {
|
/* Problem page specific styles */
|
||||||
margin-top: 20px;
|
.problem-header {
|
||||||
text-align: right;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
form button[type="submit"] {
|
gap: 20px;
|
||||||
background-color: #27ae60;
|
}
|
||||||
color: white;
|
|
||||||
border: none;
|
.back-btn {
|
||||||
padding: 12px 30px;
|
background-color: var(--muted);
|
||||||
border-radius: 8px;
|
color: white;
|
||||||
cursor: pointer;
|
border: none;
|
||||||
font-size: 16px;
|
padding: 10px 20px;
|
||||||
font-weight: 600;
|
border-radius: 6px;
|
||||||
transition: background-color 0.3s ease;
|
cursor: pointer;
|
||||||
}
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
form button[type="submit"]:hover {
|
transition: background-color 0.3s ease;
|
||||||
background-color: #229954;
|
}
|
||||||
}
|
|
||||||
|
.back-btn:hover {
|
||||||
/* Results section */
|
background-color: var(--muted-hover);
|
||||||
b {
|
}
|
||||||
color: #2c3e50;
|
|
||||||
display: inline-block;
|
.problem-desc {
|
||||||
margin: 10px 0 5px 0;
|
background: var(--card);
|
||||||
}
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
pre {
|
box-shadow: var(--shadow);
|
||||||
background-color: #f4f4f4;
|
margin-bottom: 30px;
|
||||||
padding: 20px;
|
font-size: 1.1em;
|
||||||
border-radius: 6px;
|
line-height: 1.7;
|
||||||
border-left: 4px solid #3498db;
|
transition: background-color 0.3s ease;
|
||||||
margin: 10px 0 20px 0;
|
}
|
||||||
overflow-x: auto;
|
|
||||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
/* Editor section */
|
||||||
font-size: 14px;
|
.editor-section {
|
||||||
line-height: 1.4;
|
background: var(--card);
|
||||||
}
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
pre[style*="color:red"] {
|
box-shadow: var(--shadow);
|
||||||
border-left-color: #e74c3c;
|
margin-bottom: 30px;
|
||||||
background-color: #fdf2f2;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status messages */
|
#editor {
|
||||||
p[style*="color:green"] {
|
border: 2px solid var(--border);
|
||||||
background-color: #d4edda;
|
border-radius: 8px;
|
||||||
color: #155724;
|
margin: 20px 0;
|
||||||
padding: 15px 20px;
|
height: 400px;
|
||||||
border-radius: 6px;
|
overflow: hidden;
|
||||||
border-left: 4px solid #27ae60;
|
}
|
||||||
margin: 20px 0;
|
|
||||||
font-weight: 600;
|
.editor-actions {
|
||||||
}
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
p[style*="color:red"] {
|
}
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
form button[type="submit"] {
|
||||||
padding: 15px 20px;
|
background-color: var(--success);
|
||||||
border-radius: 6px;
|
color: white;
|
||||||
border-left: 4px solid #e74c3c;
|
border: none;
|
||||||
margin: 20px 0;
|
padding: 12px 30px;
|
||||||
font-weight: 600;
|
border-radius: 8px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
/* Back to Problems link */
|
font-weight: 600;
|
||||||
a[href="/"] {
|
transition: background-color 0.3s ease;
|
||||||
display: inline-block;
|
}
|
||||||
margin-top: 30px;
|
|
||||||
background-color: #6c757d;
|
form button[type="submit"]:hover {
|
||||||
color: white;
|
background-color: var(--success-hover);
|
||||||
padding: 10px 20px;
|
}
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
/* Results section */
|
||||||
}
|
b {
|
||||||
|
color: var(--heading);
|
||||||
a[href="/"]:hover {
|
display: inline-block;
|
||||||
background-color: #5a6268;
|
margin: 10px 0 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
pre {
|
||||||
@media (max-width: 768px) {
|
background-color: var(--code-bg);
|
||||||
body {
|
padding: 20px;
|
||||||
padding: 15px;
|
border-radius: 6px;
|
||||||
}
|
border-left: 4px solid var(--accent);
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
.problem-header {
|
overflow-x: auto;
|
||||||
flex-direction: column;
|
font-family: "JetBrains Mono", "Courier New", monospace;
|
||||||
align-items: flex-start;
|
font-size: 14px;
|
||||||
gap: 15px;
|
line-height: 1.4;
|
||||||
}
|
border: 1px solid var(--border);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
h1 {
|
}
|
||||||
font-size: 1.8em;
|
|
||||||
}
|
pre[style*="color:red"] {
|
||||||
|
border-left-color: var(--error);
|
||||||
.problem-desc, .editor-section, ul {
|
background-color: var(--error-bg);
|
||||||
padding: 20px;
|
}
|
||||||
}
|
|
||||||
|
/* Status messages */
|
||||||
#editor {
|
p[style*="color:green"] {
|
||||||
height: 300px;
|
background-color: var(--success-bg);
|
||||||
}
|
color: var(--success-text);
|
||||||
|
padding: 15px 20px;
|
||||||
.editor-actions {
|
border-radius: 6px;
|
||||||
text-align: center;
|
border-left: 4px solid var(--success);
|
||||||
}
|
margin: 20px 0;
|
||||||
}
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p[style*="color:red"] {
|
||||||
|
background-color: var(--error-bg);
|
||||||
|
color: var(--error-text);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid var(--error);
|
||||||
|
margin: 20px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to Problems link */
|
||||||
|
a[href="/"] {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 30px;
|
||||||
|
background-color: var(--muted);
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href="/"]:hover {
|
||||||
|
background-color: var(--muted-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-desc,
|
||||||
|
.editor-section,
|
||||||
|
ul {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/templates/index.html
Normal file
146
src/templates/index.html
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Quick Problem Platform</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
|
||||||
|
</head>
|
||||||
|
<style>
|
||||||
|
/* Popout explanation */
|
||||||
|
#rankingExplanation {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease, opacity 0.4s ease, padding 0.4s ease;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
#rankingExplanation.active {
|
||||||
|
max-height: 800px;
|
||||||
|
opacity: 1;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
#rankInfoBtn.active { color: #2563eb; cursor:pointer; transition: transform 0.3s ease; }
|
||||||
|
#rankInfoBtn.active { transform: rotate(90deg); }
|
||||||
|
|
||||||
|
/* Highlight top rank */
|
||||||
|
.rank-1 td:first-child { font-weight: bold; }
|
||||||
|
.sort-asc::after { content: " ↑"; }
|
||||||
|
.sort-desc::after { content: " ↓"; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>Quick Problem Platform</h1>
|
||||||
|
<button id="darkModeToggle" class="dark-mode-toggle" title="Toggle dark mode">
|
||||||
|
<span class="dark-mode-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content" id="contentContainer">
|
||||||
|
<!-- Problems -->
|
||||||
|
<section class="card problems-list">
|
||||||
|
<div class="search-controls">
|
||||||
|
<input type="text" class="search-input" id="problemSearch" placeholder="Search problems..." />
|
||||||
|
</div>
|
||||||
|
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
|
||||||
|
<div id="problemsContainer">
|
||||||
|
{% for folder, description, test_code, difficulty in problems %}
|
||||||
|
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}" data-difficulty="{{ difficulty|lower }}">
|
||||||
|
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
|
||||||
|
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="problem-item">No problems yet.</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="pagination-controls" id="problemsPagination">
|
||||||
|
<button class="pagination-btn" id="problemsPrevBtn" disabled>← Previous</button>
|
||||||
|
<span class="pagination-info" id="problemsPaginationInfo">Page 1 of 1</span>
|
||||||
|
<button class="pagination-btn" id="problemsNextBtn" disabled>Next →</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Leaderboard -->
|
||||||
|
<section class="card" id="leaderboardSection">
|
||||||
|
<div class="leaderboard-head">
|
||||||
|
<h2 style="font-size:1.1rem;margin:0">Leaderboard
|
||||||
|
<span id="rankInfoBtn" title="How ranking works">ℹ️</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="leaderboard-controls">
|
||||||
|
<input type="text" class="search-input" id="problemFilter" placeholder="Filter by problem..." />
|
||||||
|
<select class="filter-select" id="runtimeFilter">
|
||||||
|
<option value="">All runtimes</option>
|
||||||
|
<option value="best">Best runtime</option>
|
||||||
|
<option value="worst">Worst runtime</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="leaderboardContainer">
|
||||||
|
<table class="leaderboard-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sortable" data-sort="rank">Rank</th>
|
||||||
|
<th class="sortable" data-sort="user">User</th>
|
||||||
|
<th class="sortable" data-sort="problem">Problem</th>
|
||||||
|
<th class="sortable" data-sort="runtime">Runtime (s)</th>
|
||||||
|
<th class="sortable" data-sort="memory">Memory (KB)</th>
|
||||||
|
<th>Line</th>
|
||||||
|
<th class="sortable" data-sort="timestamp">Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="leaderboardBody">
|
||||||
|
{% for entry in leaderboard %}
|
||||||
|
<tr data-user="{{ entry[0] }}" data-problem="{{ problem_titles.get(entry[1], 'Unknown') }}"
|
||||||
|
data-runtime="{{ '%.4f'|format(entry[2]) }}" data-memory="{{ entry[3] }}"
|
||||||
|
data-timestamp="{{ entry[5] }}">
|
||||||
|
<td>{{ loop.index }}</td>
|
||||||
|
<td>{{ entry[0] }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/problem/{{ problem_titles.get(entry[1], 'Unknown') }}"
|
||||||
|
style="color:#2563eb; text-decoration: none;"
|
||||||
|
onmouseover="this.style.textDecoration='underline';"
|
||||||
|
onmouseout="this.style.textDecoration='none';">
|
||||||
|
{{ problem_titles.get(entry[1], 'Unknown') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ '%.4f'|format(entry[2]) }}</td>
|
||||||
|
<td>{{ entry[3] }}</td>
|
||||||
|
<td>{{ entry[4] if entry[4] else '-' }}</td>
|
||||||
|
<td>{{ entry[5] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="7">No leaderboard entries yet.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ranking explanation -->
|
||||||
|
<section class="card" id="rankingExplanation">
|
||||||
|
<h2 style="font-size:1.1rem;margin-bottom:6px">How Ranking Works</h2>
|
||||||
|
<p>The leaderboard uses a <strong>weighted scoring system</strong> to determine overall rank:</p>
|
||||||
|
<ul style="margin-left: 15px;">
|
||||||
|
<li><strong>Runtime:</strong> How fast the solution runs (lower is better).</li>
|
||||||
|
<li><strong>Memory Usage:</strong> How much memory the solution uses (lower is better).</li>
|
||||||
|
</ul>
|
||||||
|
<p>Overall score is calculated as:</p>
|
||||||
|
<div style="background:#f9f9f9; border-left:4px solid #007acc; padding:1rem; margin:1rem 0;">
|
||||||
|
<code>
|
||||||
|
runtimeScore = yourRuntime / bestRuntime<br>
|
||||||
|
memoryScore = yourMemory / bestMemory<br>
|
||||||
|
overallScore = runtimeScore × 0.7 + memoryScore × 0.3
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p>Lower overall scores are better. If scores are equal, earlier submission ranks higher.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
158
src/templates/problem.html
Normal file
158
src/templates/problem.html
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{ problem.title }} - Coding Problem</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
|
<link rel="stylesheet" href="/static/problem.css" />
|
||||||
|
<!-- this is stoopid fucking html link for favicon. just cause of flask-->
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/x-icon"
|
||||||
|
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.cdnfonts.com/css/jetbrains-mono"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="problem-panel">
|
||||||
|
<div class="problem-header">
|
||||||
|
<button class="back-btn" onclick="window.location.href='/'">
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
<h1>{{ problem.title }}</h1>
|
||||||
|
<button
|
||||||
|
id="darkModeToggle"
|
||||||
|
class="dark-mode-toggle"
|
||||||
|
title="Toggle dark mode"
|
||||||
|
>
|
||||||
|
<span class="dark-mode-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="problem-desc">
|
||||||
|
{{ problem.description | safe | markdown }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="editor-header">
|
||||||
|
<h2 style="margin: 0; font-size: 18px">
|
||||||
|
Submit Your Solution (Python)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<form method="post">
|
||||||
|
<label for="username">Username (optional):</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
placeholder="Anonymous"
|
||||||
|
/>
|
||||||
|
<div id="editor"></div>
|
||||||
|
<textarea
|
||||||
|
name="user_code"
|
||||||
|
id="user_code"
|
||||||
|
style="display: none"
|
||||||
|
></textarea>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button type="submit">Run & Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="result-panel">
|
||||||
|
<h3>Result</h3>
|
||||||
|
{% if result %}
|
||||||
|
<p>
|
||||||
|
<b>Runtime:</b> {{ '%.4f'|format(result.runtime) }}
|
||||||
|
seconds
|
||||||
|
</p>
|
||||||
|
<p><b>Output:</b></p>
|
||||||
|
<pre>{{ result.output }}</pre>
|
||||||
|
{% if result.error %}
|
||||||
|
<p><b>Error:</b></p>
|
||||||
|
<pre>{{ result.error }}</pre>
|
||||||
|
{% endif %} {% else %}
|
||||||
|
<div class="placeholder">
|
||||||
|
Your code execution results will appear here
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||||
|
<script>
|
||||||
|
// Dark mode functionality
|
||||||
|
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
// Load saved dark mode preference
|
||||||
|
const savedDarkMode = localStorage.getItem("darkMode");
|
||||||
|
if (
|
||||||
|
savedDarkMode === "true" ||
|
||||||
|
(savedDarkMode === null &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
) {
|
||||||
|
html.classList.add("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
darkModeToggle.addEventListener("click", () => {
|
||||||
|
html.classList.toggle("dark");
|
||||||
|
localStorage.setItem(
|
||||||
|
"darkMode",
|
||||||
|
html.classList.contains("dark"),
|
||||||
|
);
|
||||||
|
// Update Monaco editor theme
|
||||||
|
if (window.monacoEditor) {
|
||||||
|
const isDark = html.classList.contains("dark");
|
||||||
|
window.monacoEditor.updateOptions({
|
||||||
|
theme: isDark ? "vs-dark" : "vs-light",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
require.config({
|
||||||
|
paths: {
|
||||||
|
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
require(["vs/editor/editor.main"], function () {
|
||||||
|
const isDark = html.classList.contains("dark");
|
||||||
|
window.monacoEditor = monaco.editor.create(
|
||||||
|
document.getElementById("editor"),
|
||||||
|
{
|
||||||
|
value: "",
|
||||||
|
language: "python",
|
||||||
|
theme: isDark ? "vs-dark" : "vs-light",
|
||||||
|
fontFamily: "JetBrains Mono, monospace",
|
||||||
|
fontLigatures: true,
|
||||||
|
automaticLayout: true,
|
||||||
|
fontSize: 16,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
document
|
||||||
|
.querySelector("form")
|
||||||
|
.addEventListener("submit", function (e) {
|
||||||
|
var code = window.monacoEditor.getValue();
|
||||||
|
if (!code.trim()) {
|
||||||
|
alert("Please enter your code before submitting.");
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
document.getElementById("user_code").value = code;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
262
src/utils.py
Normal file
262
src/utils.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
import io
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import ast
|
||||||
|
|
||||||
|
# Security configuration
|
||||||
|
ALLOWED_IMPORTS = {
|
||||||
|
'math', 'random', 'datetime', 'json', 'collections', 'itertools',
|
||||||
|
'functools', 'operator', 'copy', 'unittest', 're', 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
DANGEROUS_PATTERNS = [
|
||||||
|
r'import\s+os(?:\s|$|\.)',
|
||||||
|
r'from\s+os\s+import',
|
||||||
|
r'import\s+subprocess(?:\s|$|\.)',
|
||||||
|
r'from\s+subprocess\s+import',
|
||||||
|
r'import\s+sys(?:\s|$|\.)',
|
||||||
|
r'from\s+sys\s+import',
|
||||||
|
r'import\s+shutil(?:\s|$|\.)',
|
||||||
|
r'from\s+shutil\s+import',
|
||||||
|
r'import\s+pathlib(?:\s|$|\.)',
|
||||||
|
r'from\s+pathlib\s+import',
|
||||||
|
r'__import__\s*\(',
|
||||||
|
r'exec\s*\(',
|
||||||
|
r'eval\s*\(',
|
||||||
|
r'compile\s*\(',
|
||||||
|
r'open\s*\(',
|
||||||
|
r'file\s*\(',
|
||||||
|
r'input\s*\(',
|
||||||
|
r'raw_input\s*\(',
|
||||||
|
r'\.unlink\s*\(',
|
||||||
|
r'\.remove\s*\(',
|
||||||
|
r'\.rmdir\s*\(',
|
||||||
|
r'\.rmtree\s*\(',
|
||||||
|
r'\.delete\s*\(',
|
||||||
|
r'\.kill\s*\(',
|
||||||
|
r'\.terminate\s*\(',
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate_code_security(code):
|
||||||
|
"""
|
||||||
|
Validates code for security issues.
|
||||||
|
Returns (is_safe, error_message)
|
||||||
|
"""
|
||||||
|
# Check for dangerous patterns
|
||||||
|
for pattern in DANGEROUS_PATTERNS:
|
||||||
|
if re.search(pattern, code, re.IGNORECASE):
|
||||||
|
return False, f"Dangerous operation detected: {pattern}"
|
||||||
|
|
||||||
|
# Parse AST to check imports
|
||||||
|
try:
|
||||||
|
tree = ast.parse(code)
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
module_name = alias.name.split('.')[0]
|
||||||
|
if module_name not in ALLOWED_IMPORTS:
|
||||||
|
return False, f"Import not allowed: {module_name}"
|
||||||
|
elif isinstance(node, ast.ImportFrom):
|
||||||
|
if node.module:
|
||||||
|
module_name = node.module.split('.')[0]
|
||||||
|
if module_name not in ALLOWED_IMPORTS:
|
||||||
|
return False, f"Import not allowed: {module_name}"
|
||||||
|
except SyntaxError as e:
|
||||||
|
return False, f"Syntax error in code: {str(e)}"
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
def create_restricted_globals():
|
||||||
|
"""Create a restricted global namespace for code execution."""
|
||||||
|
safe_builtins = {
|
||||||
|
'abs', 'all', 'any', 'bin', 'bool', 'chr', 'dict', 'dir',
|
||||||
|
'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset',
|
||||||
|
'hex', 'id', 'int', 'isinstance', 'issubclass', 'iter', 'len',
|
||||||
|
'list', 'map', 'max', 'min', 'next', 'oct', 'ord', 'pow',
|
||||||
|
'print', 'range', 'repr', 'reversed', 'round', 'set', 'slice',
|
||||||
|
'sorted', 'str', 'sum', 'tuple', 'type', 'zip'
|
||||||
|
}
|
||||||
|
|
||||||
|
restricted_globals = {
|
||||||
|
'__builtins__': {name: __builtins__[name] for name in safe_builtins if name in __builtins__}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add allowed modules
|
||||||
|
for module in ALLOWED_IMPORTS:
|
||||||
|
try:
|
||||||
|
restricted_globals[module] = __import__(module)
|
||||||
|
except ImportError:
|
||||||
|
pass # Module not available
|
||||||
|
|
||||||
|
return restricted_globals
|
||||||
|
|
||||||
|
def run_code_against_tests(user_code, test_code, max_execution_time=5):
|
||||||
|
"""
|
||||||
|
Securely run user code against test code with safety restrictions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_code: The user's solution code
|
||||||
|
test_code: The test code to validate the solution
|
||||||
|
max_execution_time: Maximum execution time in seconds (default: 5)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result containing passed, output, runtime, and error information
|
||||||
|
"""
|
||||||
|
# Validate security for both user code and test code
|
||||||
|
user_safe, user_error = validate_code_security(user_code)
|
||||||
|
if not user_safe:
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'output': '',
|
||||||
|
'runtime': 0,
|
||||||
|
'error': f"Security violation in user code: {user_error}"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_safe, test_error = validate_code_security(test_code)
|
||||||
|
if not test_safe:
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'output': '',
|
||||||
|
'runtime': 0,
|
||||||
|
'error': f"Security violation in test code: {test_error}"
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ns = {}
|
||||||
|
output = ''
|
||||||
|
start = time.perf_counter()
|
||||||
|
error = None
|
||||||
|
passed = False
|
||||||
|
temp_file = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if unittest is used in test_code
|
||||||
|
if 'unittest' in test_code:
|
||||||
|
# Create temp file in a secure temporary directory
|
||||||
|
temp_dir = tempfile.mkdtemp(prefix='secure_code_')
|
||||||
|
try:
|
||||||
|
temp_file = os.path.join(temp_dir, 'test_code.py')
|
||||||
|
combined_code = f"{user_code}\n\n{test_code}"
|
||||||
|
|
||||||
|
# Write to temp file with restricted permissions
|
||||||
|
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(combined_code)
|
||||||
|
os.chmod(temp_file, 0o600) # Read/write for owner only
|
||||||
|
|
||||||
|
# Run the file as a subprocess with additional security
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, temp_file],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=max_execution_time,
|
||||||
|
encoding='utf-8',
|
||||||
|
cwd=temp_dir, # Run in the temporary directory
|
||||||
|
env={'PYTHONPATH': ''} # Restrict Python path
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine both stdout and stderr to capture all output
|
||||||
|
combined_output = ""
|
||||||
|
if proc.stdout:
|
||||||
|
combined_output += proc.stdout
|
||||||
|
if proc.stderr:
|
||||||
|
if combined_output:
|
||||||
|
combined_output += "\n" + proc.stderr
|
||||||
|
else:
|
||||||
|
combined_output = proc.stderr
|
||||||
|
|
||||||
|
output = combined_output
|
||||||
|
passed = proc.returncode == 0
|
||||||
|
|
||||||
|
if not passed:
|
||||||
|
error = f"Tests failed. Return code: {proc.returncode}\n{output}"
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
passed = False
|
||||||
|
error = f"Code execution timed out after {max_execution_time} seconds"
|
||||||
|
output = "Execution timed out"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Secure cleanup of temporary directory and files
|
||||||
|
try:
|
||||||
|
if temp_file and os.path.exists(temp_file):
|
||||||
|
os.chmod(temp_file, 0o600) # Ensure we can delete
|
||||||
|
os.unlink(temp_file)
|
||||||
|
if os.path.exists(temp_dir):
|
||||||
|
os.rmdir(temp_dir)
|
||||||
|
except Exception as cleanup_error:
|
||||||
|
print(f"Warning: Could not clean up temp files: {cleanup_error}")
|
||||||
|
else:
|
||||||
|
# Direct execution with restricted globals
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
captured_output = io.StringIO()
|
||||||
|
sys.stdout = captured_output
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create restricted execution environment
|
||||||
|
restricted_globals = create_restricted_globals()
|
||||||
|
|
||||||
|
# Execute user code in restricted environment
|
||||||
|
exec(user_code, restricted_globals, local_ns)
|
||||||
|
|
||||||
|
# Execute test code (should raise AssertionError if fail)
|
||||||
|
exec(test_code, {**restricted_globals, **local_ns}, local_ns)
|
||||||
|
passed = True
|
||||||
|
|
||||||
|
except AssertionError as e:
|
||||||
|
passed = False
|
||||||
|
error = f"Assertion failed: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
passed = False
|
||||||
|
error = f"Runtime error: {traceback.format_exc()}"
|
||||||
|
finally:
|
||||||
|
output = captured_output.getvalue()
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
passed = False
|
||||||
|
error = f"Execution error: {traceback.format_exc()}"
|
||||||
|
|
||||||
|
runtime = time.perf_counter() - start
|
||||||
|
|
||||||
|
# Limit output size to prevent memory issues
|
||||||
|
max_output_size = 10000 # 10KB limit
|
||||||
|
if len(output) > max_output_size:
|
||||||
|
output = output[:max_output_size] + "\n... (output truncated)"
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'passed': passed,
|
||||||
|
'output': output.strip() if output else '',
|
||||||
|
'runtime': runtime,
|
||||||
|
'error': error if not passed else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Example usage with additional safety wrapper
|
||||||
|
def safe_code_runner(user_code, test_code):
|
||||||
|
"""
|
||||||
|
Additional wrapper for extra safety checks.
|
||||||
|
"""
|
||||||
|
# Additional length checks
|
||||||
|
if len(user_code) > 50000: # 50KB limit
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'output': '',
|
||||||
|
'runtime': 0,
|
||||||
|
'error': "User code too large (maximum 50KB allowed)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(test_code) > 10000: # 10KB limit for test code
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'output': '',
|
||||||
|
'runtime': 0,
|
||||||
|
'error': "Test code too large (maximum 10KB allowed)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return run_code_against_tests(user_code, test_code)
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Quick Problem Platform</title>
|
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
|
||||||
<style>
|
|
||||||
.leaderboard-table {
|
|
||||||
width: auto;
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: 600px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 2em 0;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 2px 8px #0001;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.leaderboard-table th, .leaderboard-table td {
|
|
||||||
padding: 0.7em 1em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.leaderboard-table th {
|
|
||||||
background: #f5f5f5;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.leaderboard-table tr:nth-child(even) {
|
|
||||||
background: #fafbfc;
|
|
||||||
}
|
|
||||||
.leaderboard-table tr:nth-child(odd) {
|
|
||||||
background: #f0f2f5;
|
|
||||||
}
|
|
||||||
.leaderboard-table td {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
.leaderboard-table tr:hover {
|
|
||||||
background: #e0e7ff;
|
|
||||||
}
|
|
||||||
.problems-list {
|
|
||||||
max-width: 600px;
|
|
||||||
min-width: 400px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.problems-list ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.problems-list li {
|
|
||||||
padding: 0.5em 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Quick Problem Platform</h1>
|
|
||||||
<!--<a href="/problem/new">Submit New Problem</a>-->
|
|
||||||
<section class="problems-list">
|
|
||||||
<h2>Problems</h2>
|
|
||||||
<ul>
|
|
||||||
{% for problem in problems %}
|
|
||||||
<li><a href="/problem/{{ problem.id }}">{{ problem.title }}</a></li>
|
|
||||||
{% else %}
|
|
||||||
<li>No problems yet.</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Leaderboard</h2>
|
|
||||||
<table class="leaderboard-table">
|
|
||||||
<tr>
|
|
||||||
<th>Rank</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Problem</th>
|
|
||||||
<th>Runtime (s)</th>
|
|
||||||
<th>Memory (KB)</th>
|
|
||||||
<th>Line Number</th>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
</tr>
|
|
||||||
{% for entry in leaderboard %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ loop.index }}</td>
|
|
||||||
<td>{{ entry[0] }}</td>
|
|
||||||
<td>{{ problem_titles.get(entry[1], 'Unknown') }}</td>
|
|
||||||
<td>{{ '%.4f'|format(entry[2]) }}</td>
|
|
||||||
<td>{{ entry[3] }}</td>
|
|
||||||
<td>{{ entry[4] if entry[4] else '-' }}</td>
|
|
||||||
<td>{{ entry[5] }}</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr><td colspan="7">No leaderboard entries yet.</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ problem.title }}</title>
|
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
|
||||||
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="problem-header">
|
|
||||||
<button class="back-btn" onclick="window.location.href='/'">← Back</button>
|
|
||||||
<h1 style="margin-bottom:0;">{{ problem.title }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="problem-desc">{{ problem.description }}</div>
|
|
||||||
<div class="editor-section" style="max-width:1160;margin:0">
|
|
||||||
<h2 style="margin-top:0;">Submit Your Solution (Python)</h2>
|
|
||||||
<form method="post">
|
|
||||||
<label for="username">Username (optional):</label>
|
|
||||||
<input type="text" name="username" id="username" placeholder="Anonymous" style="margin-bottom:10px;">
|
|
||||||
<div id="editor"></div>
|
|
||||||
<textarea name="user_code" id="user_code" style="display:none;"></textarea>
|
|
||||||
<div class="editor-actions">
|
|
||||||
<button type="submit">Run & Submit</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
||||||
<script>
|
|
||||||
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
|
|
||||||
require(['vs/editor/editor.main'], function() {
|
|
||||||
var editor = monaco.editor.create(document.getElementById('editor'), {
|
|
||||||
value: '',
|
|
||||||
language: 'python',
|
|
||||||
theme: 'vs-light',
|
|
||||||
fontFamily: 'JetBrains Mono, monospace',
|
|
||||||
fontLigatures: true,
|
|
||||||
automaticLayout: true,
|
|
||||||
fontSize: 16,
|
|
||||||
minimap: { enabled: false }
|
|
||||||
});
|
|
||||||
document.querySelector('form').addEventListener('submit', function(e) {
|
|
||||||
var code = editor.getValue();
|
|
||||||
if (!code.trim()) {
|
|
||||||
alert('Please enter your code before submitting.');
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
document.getElementById('user_code').value = code;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% if result %}
|
|
||||||
<div class="editor-section" style="max-width:1160;margin:0;margin-top: 5px;">
|
|
||||||
<h3>Result:</h3>
|
|
||||||
<b>Runtime:</b> {{ '%.4f'|format(result.runtime) }} seconds<br>
|
|
||||||
<b>Output:</b>
|
|
||||||
<pre>{{ result.output }}</pre>
|
|
||||||
{% if result.error %}
|
|
||||||
<b>Error:</b>
|
|
||||||
<pre style="color:red;">{{ result.error }}</pre>
|
|
||||||
{% endif %}
|
|
||||||
{% if result.passed %}
|
|
||||||
<p style="color:green;">Passed!</p>
|
|
||||||
{% else %}
|
|
||||||
<p style="color:red;">Failed!</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<!--<a href="/">Back to Problems</a>-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
51
utils.py
51
utils.py
@@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
import io
|
|
||||||
|
|
||||||
def run_code_against_tests(user_code, test_code):
|
|
||||||
import tempfile
|
|
||||||
import subprocess
|
|
||||||
local_ns = {}
|
|
||||||
output = ''
|
|
||||||
start = time.perf_counter()
|
|
||||||
error = None
|
|
||||||
passed = False
|
|
||||||
try:
|
|
||||||
# Check if unittest is used in test_code
|
|
||||||
if 'unittest' in test_code:
|
|
||||||
# Write user code + test code to a temp file
|
|
||||||
with tempfile.NamedTemporaryFile('w+', suffix='.py', delete=False) as f:
|
|
||||||
f.write(user_code + '\n' + test_code)
|
|
||||||
f.flush()
|
|
||||||
f_name = f.name
|
|
||||||
# Run the file as a subprocess
|
|
||||||
proc = subprocess.run([sys.executable, f_name], capture_output=True, text=True, timeout=10)
|
|
||||||
output = proc.stdout + proc.stderr
|
|
||||||
passed = proc.returncode == 0
|
|
||||||
error = None if passed else output
|
|
||||||
else:
|
|
||||||
# Capture stdout
|
|
||||||
old_stdout = sys.stdout
|
|
||||||
sys.stdout = mystdout = io.StringIO()
|
|
||||||
# Execute user code
|
|
||||||
exec(user_code, {}, local_ns)
|
|
||||||
# Execute test code (should raise AssertionError if fail)
|
|
||||||
exec(test_code, local_ns, local_ns)
|
|
||||||
passed = True
|
|
||||||
except Exception as e:
|
|
||||||
passed = False
|
|
||||||
error = traceback.format_exc()
|
|
||||||
finally:
|
|
||||||
if 'mystdout' in locals():
|
|
||||||
output = mystdout.getvalue() or output
|
|
||||||
sys.stdout = old_stdout if 'old_stdout' in locals() else sys.stdout
|
|
||||||
runtime = time.perf_counter() - start
|
|
||||||
result = {
|
|
||||||
'passed': passed,
|
|
||||||
'output': output,
|
|
||||||
'runtime': runtime,
|
|
||||||
'error': error if not passed else None
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
Reference in New Issue
Block a user