Compare commits
10 Commits
shitdontwo
...
04dc638cf0
| Author | SHA1 | Date | |
|---|---|---|---|
| 04dc638cf0 | |||
| 38c3256f19 | |||
| 1374cb9cb1 | |||
| c1ef310f6a | |||
| 525297f19b | |||
| 89ea87951e | |||
| c7c1b8ecd6 | |||
| 0bffdf612c | |||
| a03f9ddb14 | |||
| 3f1f709f30 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
__pycache__
|
||||
instance
|
||||
.venv
|
||||
*.sqlite3
|
||||
venv
|
||||
*.sqlite3
|
||||
|
||||
@@ -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()
|
||||
55
readme.md
55
readme.md
@@ -4,22 +4,59 @@
|
||||
|
||||
but more lightweight
|
||||
|
||||
run the bash script to start the server.
|
||||
|
||||
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
|
||||
"""
|
||||
@TESTSAMPLE.PY / NAME THIS "test.py" in your actual project
|
||||
"""
|
||||
import unittest
|
||||
|
||||
#<!-- The Function the User needs to write -->
|
||||
def revstring(x):
|
||||
return x[::-1]
|
||||
" )) First Point from the List "
|
||||
# def sortlist(lst = [4,3,2,1]) -> list:
|
||||
# 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):
|
||||
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])
|
||||
def test_sort(self):
|
||||
self.x = []
|
||||
self.assertEqual(sortlist(self.x), sorted(self.x)) # pyright: ignore[reportUndefinedVariable] <- This is only here so that pyright doesnt complain ; NOT NECCESARY!
|
||||
|
||||
if __name__ == "__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 @@
|
||||
#!/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,24 +1,27 @@
|
||||
from markupsafe import Markup
|
||||
from flask import Flask, render_template, request, redirect, url_for, send_from_directory
|
||||
import markdown as md
|
||||
|
||||
from models import db, Problem, Solution
|
||||
from utils import run_code_against_tests
|
||||
from leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
||||
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 problem_scanner import start_problem_scanner
|
||||
from src.problem_scanner import start_problem_scanner
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3'
|
||||
|
||||
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()
|
||||
@@ -38,16 +41,17 @@ def favicon():
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
db_path = Path(__file__).parent / 'problems.sqlite3'
|
||||
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT folder, description, test_code FROM problems')
|
||||
#<!-- 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}
|
||||
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'])
|
||||
@@ -64,20 +68,23 @@ def new_problem():
|
||||
|
||||
@app.route('/problem/<folder>', methods=['GET', 'POST'])
|
||||
def view_problem(folder):
|
||||
db_path = Path(__file__).parent / 'problems.sqlite3'
|
||||
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT folder, description, test_code FROM problems WHERE folder = ?', (folder,))
|
||||
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],
|
||||
'test_code': row[2],
|
||||
'title': row[0].replace('_', ' ').title()
|
||||
'difficulty': row[3], # now correct
|
||||
'test_code': row[2], # now correct
|
||||
}
|
||||
|
||||
result = None
|
||||
if request.method == 'POST':
|
||||
user_code = request.form['user_code']
|
||||
@@ -88,9 +95,9 @@ def view_problem(folder):
|
||||
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)
|
||||
# Find the highest line number in the AST (for multi-function/user code)
|
||||
@@ -102,6 +109,7 @@ def view_problem(folder):
|
||||
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']
|
||||
@@ -112,7 +120,11 @@ def view_problem(folder):
|
||||
line_number = tb_lines[-1].lineno
|
||||
except Exception:
|
||||
pass
|
||||
log_leaderboard(username, problem['folder'], run_result['runtime'], memory_used, line_number)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -2,12 +2,14 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from flask import g
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
if 'db' not in g:
|
||||
db_path = Path(__file__).parent / 'database' / 'db.sqlite3'
|
||||
db_path.parent.mkdir(exist_ok=True) # Ensure /database folder exists
|
||||
g.db = sqlite3.connect(db_path)
|
||||
return g.db
|
||||
|
||||
def create_leaderboard_table():
|
||||
db = get_db()
|
||||
15
src/problem_loader.py
Normal file
15
src/problem_loader.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from models import db, Problem
|
||||
from flask import current_app
|
||||
|
||||
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()
|
||||
@@ -19,7 +19,7 @@ except ImportError:
|
||||
WATCHDOG_AVAILABLE = False
|
||||
|
||||
PROBLEMS_DIR = Path(__file__).parent / 'problems'
|
||||
DB_PATH = Path(__file__).parent / 'problems.sqlite3'
|
||||
DB_PATH = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
|
||||
class ProblemScannerThread(threading.Thread):
|
||||
def __init__(self, scan_interval=2):
|
||||
@@ -35,6 +35,7 @@ class ProblemScannerThread(threading.Thread):
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
folder TEXT,
|
||||
description TEXT,
|
||||
difficulty TEXT,
|
||||
test_code TEXT
|
||||
)''')
|
||||
conn.commit()
|
||||
@@ -65,13 +66,18 @@ class ProblemScannerThread(threading.Thread):
|
||||
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
|
||||
'test_code': test_code,
|
||||
'difficulty': difficulty
|
||||
})
|
||||
print(f"Found problem: {folder.name}")
|
||||
print(f"Found problem: {folder.name} ; Difficulty: {difficulty}")
|
||||
except Exception as e:
|
||||
print(f"Error reading problem files for {folder.name}: {e}")
|
||||
else:
|
||||
@@ -99,8 +105,10 @@ class ProblemScannerThread(threading.Thread):
|
||||
|
||||
# Insert new problems
|
||||
for p in problems:
|
||||
c.execute('INSERT INTO problems (folder, description, test_code) VALUES (?, ?, ?)',
|
||||
(p['folder'], p['description'], p['test_code']))
|
||||
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")
|
||||
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))
|
||||
"""
|
||||
@@ -2,5 +2,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -73,6 +73,9 @@ header p {
|
||||
.problems-list .problem-item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.problem-item:last-child {
|
||||
border-bottom: none;
|
||||
@@ -82,6 +85,28 @@ header p {
|
||||
color: #0077ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 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;
|
||||
@@ -155,6 +180,6 @@ header p {
|
||||
@media (max-width: 800px) {
|
||||
.content { grid-template-columns: 1fr; }
|
||||
.leaderboard-controls {
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -14,29 +14,29 @@
|
||||
<header>
|
||||
<h1>Quick Problem Platform</h1>
|
||||
</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..."
|
||||
/>
|
||||
<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 }}">
|
||||
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
|
||||
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
|
||||
</div>
|
||||
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
|
||||
<div id="problemsContainer">
|
||||
{% for folder, description, test_code in problems %}
|
||||
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}">
|
||||
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="problem-item">No problems yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="problem-item">No problems yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<section class="card" id="leaderboardSection">
|
||||
296
src/templates/problem.html
Normal file
296
src/templates/problem.html
Normal file
@@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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 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">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
min-height: 100vh; /* allow content to grow */
|
||||
overflow-y: auto; /* allow vertical scroll */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *::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: white;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #eaeaea;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1 1 400px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
max-height: 100vh;
|
||||
overflow: hidden; /* internal scroll handling */
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
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;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-right: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.problem-desc {
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.problem-desc pre {
|
||||
background: #f6f8fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.problem-desc code {
|
||||
background: #f6f8fa;
|
||||
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: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions button:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1 1 auto;
|
||||
min-height: 300px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 120px;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
.result-panel h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-panel pre {
|
||||
background: #f6f8fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* 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 #eaeaea;
|
||||
}
|
||||
#editor {
|
||||
min-height: 400px;
|
||||
max-height: none;
|
||||
}
|
||||
.result-panel {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</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>
|
||||
</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>
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,66 +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 | safe | markdown }}</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>{{ result.error }}</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user