initial commit blyad
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__
|
||||||
|
instance
|
||||||
|
.venv
|
||||||
|
*.sqlite3
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# This file marks the directory as a Python package.
|
||||||
89
app.py
Normal file
89
app.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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)
|
||||||
46
leaderboard.py
Normal file
46
leaderboard.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask import g
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, '_database', None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'db.sqlite3'))
|
||||||
|
return db
|
||||||
|
|
||||||
|
def create_leaderboard_table():
|
||||||
|
db = get_db()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS leaderboard (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT,
|
||||||
|
problem_id INTEGER,
|
||||||
|
runtime REAL,
|
||||||
|
memory_used INTEGER,
|
||||||
|
line_number INTEGER,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def log_leaderboard(username, problem_id, runtime, memory_used, line_number):
|
||||||
|
db = get_db()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO leaderboard (username, problem_id, runtime, memory_used, line_number)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
''', (username, problem_id, runtime, memory_used, line_number))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def get_leaderboard():
|
||||||
|
db = get_db()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT username, problem_id, runtime, memory_used, line_number, timestamp
|
||||||
|
FROM leaderboard
|
||||||
|
ORDER BY runtime ASC, memory_used ASC
|
||||||
|
LIMIT 20
|
||||||
|
''')
|
||||||
|
return cursor.fetchall()
|
||||||
17
models.py
Normal file
17
models.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
class Problem(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(100), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=False)
|
||||||
|
test_code = db.Column(db.Text, nullable=False) # Python code to test solution
|
||||||
|
|
||||||
|
class Solution(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
problem_id = db.Column(db.Integer, db.ForeignKey('problem.id'), nullable=False)
|
||||||
|
user_code = db.Column(db.Text, nullable=False)
|
||||||
|
passed = db.Column(db.Boolean, default=False)
|
||||||
|
output = db.Column(db.Text)
|
||||||
|
problem = db.relationship('Problem', backref=db.backref('solutions', lazy=True))
|
||||||
37
problem_loader.py
Normal file
37
problem_loader.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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()
|
||||||
12
problems.json
Normal file
12
problems.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
14
problems/solution_reversed_string.py
Normal file
14
problems/solution_reversed_string.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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()
|
||||||
11
problems/sortlist.py
Normal file
11
problems/sortlist.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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()
|
||||||
25
readme.md
Normal file
25
readme.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## under construction
|
||||||
|
|
||||||
|
### Like LeetCode
|
||||||
|
|
||||||
|
but more lightweight
|
||||||
|
|
||||||
|
if you want to contribute write tests like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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):
|
||||||
|
# !! This needs to be dynamic ; if the user enters some shit then it is supposed to work too
|
||||||
|
x="";
|
||||||
|
self.assertEqual(revstring(x), x[::-1])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
```
|
||||||
260
static/style.css
Normal file
260
static/style.css
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
/* Reset and base styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main heading */
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: -10px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
border-bottom: 3px solid #3498db;
|
||||||
|
font-size: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #34495e;
|
||||||
|
margin: 30px 0 20px 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #34495e;
|
||||||
|
margin: 25px 0 15px 0;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links and buttons */
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary action link (Submit New Problem) */
|
||||||
|
a[href="/problem/new"] {
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href="/problem/new"]:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem list */
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
padding: 25px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: -12px -20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
transform: translateX(5px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem page specific styles */
|
||||||
|
.problem-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background-color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-desc {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor section */
|
||||||
|
.editor-section {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button[type="submit"] {
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button[type="submit"]:hover {
|
||||||
|
background-color: #229954;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results section */
|
||||||
|
b {
|
||||||
|
color: #2c3e50;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[style*="color:red"] {
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
background-color: #fdf2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status messages */
|
||||||
|
p[style*="color:green"] {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #27ae60;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p[style*="color:red"] {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #e74c3c;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back to Problems link */
|
||||||
|
a[href="/"] {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 30px;
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href="/"]:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
templates/index.html
Normal file
99
templates/index.html
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<!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>
|
||||||
72
templates/problem.html
Normal file
72
templates/problem.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<!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
Normal file
51
utils.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
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