shitdontwork #1
71
app.py
71
app.py
@@ -1,11 +1,17 @@
|
|||||||
|
from markupsafe import Markup
|
||||||
from flask import Flask, render_template, request, redirect, url_for
|
from flask import Flask, render_template, request, redirect, url_for
|
||||||
|
import markdown as md
|
||||||
|
|
||||||
from models import db, Problem, Solution
|
from models import db, Problem, Solution
|
||||||
from utils import run_code_against_tests
|
from utils import run_code_against_tests
|
||||||
from leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
from leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from problem_loader import load_problems_from_json, schedule_problem_reload
|
## from problem_loader import load_problems_from_json, schedule_problem_reload
|
||||||
|
from problem_scanner import start_problem_scanner
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3'
|
||||||
@@ -17,18 +23,23 @@ db.init_app(app)
|
|||||||
def setup():
|
def setup():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
create_leaderboard_table() # Ensure leaderboard table exists
|
create_leaderboard_table() # Ensure leaderboard table exists
|
||||||
# Load problems from JSON at startup
|
# Problems are now loaded from manifests by the background scanner. No need to load problems.json.
|
||||||
json_path = os.path.join(os.path.dirname(__file__), 'problems.json')
|
|
||||||
load_problems_from_json(json_path)
|
# Start the background thread to scan problems
|
||||||
# Schedule reload every 10 hours
|
start_problem_scanner()
|
||||||
schedule_problem_reload(app, json_path, interval_hours=10)
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
problems = Problem.query.all()
|
db_path = Path(__file__).parent / 'problems.sqlite3'
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT folder, description, test_code FROM problems')
|
||||||
|
problems = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
# Get leaderboard entries
|
||||||
leaderboard = get_leaderboard()
|
leaderboard = get_leaderboard()
|
||||||
# Map problem_id to title for leaderboard display
|
# Map folder to title for display
|
||||||
problem_titles = {p.id: p.title for p 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)
|
return render_template('index.html', problems=problems, leaderboard=leaderboard, problem_titles=problem_titles)
|
||||||
|
|
||||||
@app.route('/problem/new', methods=['GET', 'POST'])
|
@app.route('/problem/new', methods=['GET', 'POST'])
|
||||||
@@ -43,16 +54,29 @@ def new_problem():
|
|||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
return render_template('new_problem.html')
|
return render_template('new_problem.html')
|
||||||
|
|
||||||
@app.route('/problem/<int:problem_id>', methods=['GET', 'POST'])
|
@app.route('/problem/<folder>', methods=['GET', 'POST'])
|
||||||
def view_problem(problem_id):
|
def view_problem(folder):
|
||||||
problem = Problem.query.get_or_404(problem_id)
|
db_path = Path(__file__).parent / 'problems.sqlite3'
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT folder, description, test_code 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()
|
||||||
|
}
|
||||||
result = None
|
result = None
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
user_code = request.form['user_code']
|
user_code = request.form['user_code']
|
||||||
username = request.form.get('username', '').strip() or 'Anonymous'
|
username = request.form.get('username', '').strip() or 'Anonymous'
|
||||||
import tracemalloc
|
import tracemalloc
|
||||||
tracemalloc.start()
|
tracemalloc.start()
|
||||||
run_result = run_code_against_tests(user_code, problem.test_code)
|
run_result = run_code_against_tests(user_code, problem['test_code'])
|
||||||
current, peak = tracemalloc.get_traced_memory()
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
tracemalloc.stop()
|
tracemalloc.stop()
|
||||||
memory_used = peak // 1024 # in KB
|
memory_used = peak // 1024 # in KB
|
||||||
@@ -61,10 +85,13 @@ def view_problem(problem_id):
|
|||||||
import ast
|
import ast
|
||||||
try:
|
try:
|
||||||
tree = ast.parse(user_code)
|
tree = ast.parse(user_code)
|
||||||
# Get the last line number in the user's code
|
# Find the highest line number in the AST (for multi-function/user code)
|
||||||
if hasattr(tree, 'body') and tree.body:
|
def get_max_lineno(node):
|
||||||
last_node = tree.body[-1]
|
max_lineno = getattr(node, 'lineno', 0)
|
||||||
line_number = getattr(last_node, 'lineno', None)
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# If there was an error, try to get the error line number from the traceback
|
# If there was an error, try to get the error line number from the traceback
|
||||||
@@ -77,13 +104,13 @@ def view_problem(problem_id):
|
|||||||
line_number = tb_lines[-1].lineno
|
line_number = tb_lines[-1].lineno
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
log_leaderboard(username, problem.id, run_result['runtime'], memory_used, line_number)
|
log_leaderboard(username, problem['folder'], 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
|
result = run_result
|
||||||
|
|
||||||
return render_template('problem.html', problem=problem, result=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__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
118
problem_scanner.py
Normal file
118
problem_scanner.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import threading
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
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 / '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,
|
||||||
|
test_code TEXT
|
||||||
|
)''')
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def scan(self):
|
||||||
|
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'
|
||||||
|
if manifest_path and test_path.exists():
|
||||||
|
with open(desc_path, 'r') as f:
|
||||||
|
description = f.read()
|
||||||
|
with open(test_path, 'r') as f:
|
||||||
|
test_code = f.read()
|
||||||
|
problems.append({
|
||||||
|
'folder': folder.name,
|
||||||
|
'description': description,
|
||||||
|
'test_code': test_code
|
||||||
|
})
|
||||||
|
return problems
|
||||||
|
|
||||||
|
def update_db(self, problems, retries=5):
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_PATH, timeout=2)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('PRAGMA journal_mode=WAL;')
|
||||||
|
c.execute('DELETE FROM problems')
|
||||||
|
for p in problems:
|
||||||
|
c.execute('INSERT INTO problems (folder, description, test_code) VALUES (?, ?, ?)',
|
||||||
|
(p['folder'], p['description'], p['test_code']))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if 'locked' in str(e):
|
||||||
|
time.sleep(0.2 + random.random() * 0.3)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
print('Failed to update problems DB after several retries due to lock.')
|
||||||
|
|
||||||
|
def rescan_and_update(self):
|
||||||
|
problems = self.scan()
|
||||||
|
self.update_db(problems)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Initial scan and table creation
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
self.create_table(conn)
|
||||||
|
conn.close()
|
||||||
|
self.rescan_and_update()
|
||||||
|
if WATCHDOG_AVAILABLE:
|
||||||
|
class Handler(FileSystemEventHandler):
|
||||||
|
def __init__(self, scanner):
|
||||||
|
self.scanner = scanner
|
||||||
|
def on_any_event(self, event):
|
||||||
|
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)
|
||||||
|
finally:
|
||||||
|
self.observer.stop()
|
||||||
|
self.observer.join()
|
||||||
|
else:
|
||||||
|
# Fallback: poll every scan_interval seconds
|
||||||
|
while True:
|
||||||
|
time.sleep(self.scan_interval)
|
||||||
|
self.rescan_and_update()
|
||||||
|
|
||||||
|
def start_problem_scanner():
|
||||||
|
scanner = ProblemScannerThread()
|
||||||
|
scanner.start()
|
||||||
|
return scanner
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
29
problems/reversedstring/description.md
Normal file
29
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
|
||||||
6
problems/reversedstring/manifest.json
Normal file
6
problems/reversedstring/manifest.json
Normal file
@@ -0,0 +1,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",
|
||||||
|
"test_code":"problems/reversedstring/test.py"
|
||||||
|
}
|
||||||
19
problems/reversedstring/test.py
Normal file
19
problems/reversedstring/test.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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 = "Hello World"
|
||||||
|
self.assertEqual(revstring(x), "dlroW olleH") # Test simple string reversal
|
||||||
|
self.assertEqual(revstring(""), "") # Test empty string
|
||||||
|
self.assertEqual(revstring("a"), "a") # Test single character
|
||||||
|
self.assertEqual(revstring("racecar"), "racecar") # Test palindrome
|
||||||
|
self.assertEqual(revstring("12345"), "54321") # Test numbers as string
|
||||||
|
self.assertEqual(revstring("!@# $%"), "%$ #@!") # Test special chars and spaces
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -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
problems/sortlist/description.md
Normal file
1
problems/sortlist/description.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
this is a easy sorting problem **it is solvable in less than 2 seconds**
|
||||||
6
problems/sortlist/manifets.json
Normal file
6
problems/sortlist/manifets.json
Normal file
@@ -0,0 +1,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",
|
||||||
|
"test_code": "problems/sortlist/test.py"
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import unittest
|
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):
|
class TestSolution(unittest.TestCase):
|
||||||
def test_sort(self):
|
def test_sort(self):
|
||||||
# define x as a empty array.
|
# define x as a empty array.
|
||||||
@@ -61,8 +61,10 @@
|
|||||||
<section class="problems-list">
|
<section class="problems-list">
|
||||||
<h2>Problems</h2>
|
<h2>Problems</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for problem in problems %}
|
{% for folder, description, test_code in problems %}
|
||||||
<li><a href="/problem/{{ problem.id }}">{{ problem.title }}</a></li>
|
<li>
|
||||||
|
<a href="/problem/{{ folder }}"><b>{{ folder.replace('_', ' ').title() }}</b></a>
|
||||||
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>No problems yet.</li>
|
<li>No problems yet.</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<button class="back-btn" onclick="window.location.href='/'">← Back</button>
|
<button class="back-btn" onclick="window.location.href='/'">← Back</button>
|
||||||
<h1 style="margin-bottom:0;">{{ problem.title }}</h1>
|
<h1 style="margin-bottom:0;">{{ problem.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="problem-desc">{{ problem.description }}</div>
|
<div class="problem-desc">{{ problem.description | safe | markdown }}</div>
|
||||||
<div class="editor-section" style="max-width:1160;margin:0">
|
<div class="editor-section" style="max-width:1160;margin:0">
|
||||||
<h2 style="margin-top:0;">Submit Your Solution (Python)</h2>
|
<h2 style="margin-top:0;">Submit Your Solution (Python)</h2>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -58,15 +58,9 @@
|
|||||||
<pre>{{ result.output }}</pre>
|
<pre>{{ result.output }}</pre>
|
||||||
{% if result.error %}
|
{% if result.error %}
|
||||||
<b>Error:</b>
|
<b>Error:</b>
|
||||||
<pre style="color:red;">{{ result.error }}</pre>
|
<pre>{{ result.error }}</pre>
|
||||||
{% endif %}
|
|
||||||
{% if result.passed %}
|
|
||||||
<p style="color:green;">Passed!</p>
|
|
||||||
{% else %}
|
|
||||||
<p style="color:red;">Failed!</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!--<a href="/">Back to Problems</a>-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user