From d342f888a92a1f80b888fa9664e04d61060baae5 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 11 Aug 2025 21:49:33 +0200 Subject: [PATCH] initial commit blyad --- .gitignore | 4 + __init__.py | 1 + app.py | 89 +++++++++ leaderboard.py | 46 +++++ models.py | 17 ++ problem_loader.py | 37 ++++ problems.json | 12 ++ problems/solution_reversed_string.py | 14 ++ problems/sortlist.py | 11 ++ readme.md | 25 +++ static/style.css | 260 +++++++++++++++++++++++++++ templates/index.html | 99 ++++++++++ templates/problem.html | 72 ++++++++ utils.py | 51 ++++++ 14 files changed, 738 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 app.py create mode 100644 leaderboard.py create mode 100644 models.py create mode 100644 problem_loader.py create mode 100644 problems.json create mode 100644 problems/solution_reversed_string.py create mode 100644 problems/sortlist.py create mode 100644 readme.md create mode 100644 static/style.css create mode 100644 templates/index.html create mode 100644 templates/problem.html create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97f71e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +instance +.venv +*.sqlite3 \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0145fca --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +# This file marks the directory as a Python package. diff --git a/app.py b/app.py new file mode 100644 index 0000000..728e727 --- /dev/null +++ b/app.py @@ -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/', 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) diff --git a/leaderboard.py b/leaderboard.py new file mode 100644 index 0000000..6f8516e --- /dev/null +++ b/leaderboard.py @@ -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() diff --git a/models.py b/models.py new file mode 100644 index 0000000..77b9840 --- /dev/null +++ b/models.py @@ -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)) diff --git a/problem_loader.py b/problem_loader.py new file mode 100644 index 0000000..09d5d51 --- /dev/null +++ b/problem_loader.py @@ -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() diff --git a/problems.json b/problems.json new file mode 100644 index 0000000..2d6070f --- /dev/null +++ b/problems.json @@ -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" + } +] diff --git a/problems/solution_reversed_string.py b/problems/solution_reversed_string.py new file mode 100644 index 0000000..93f2872 --- /dev/null +++ b/problems/solution_reversed_string.py @@ -0,0 +1,14 @@ +import unittest + +# +def revstring(x): + return x[::-1] + +# +class TestSolution(unittest.TestCase): + def test_simple(self): + x=""; + self.assertEqual(revstring(x), x[::-1]) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/problems/sortlist.py b/problems/sortlist.py new file mode 100644 index 0000000..6ace00c --- /dev/null +++ b/problems/sortlist.py @@ -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() \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d31150b --- /dev/null +++ b/readme.md @@ -0,0 +1,25 @@ +## under construction + +### Like LeetCode + +but more lightweight + +if you want to contribute write tests like this: + +```python +import unittest + +# +def revstring(x): + return x[::-1] + +# +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() +``` \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b576b4a --- /dev/null +++ b/static/style.css @@ -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; + } +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a18b5e7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,99 @@ + + + + + + Quick Problem Platform + + + + +

Quick Problem Platform

+ +
+

Problems

+
    + {% for problem in problems %} +
  • {{ problem.title }}
  • + {% else %} +
  • No problems yet.
  • + {% endfor %} +
+
+
+

Leaderboard

+ + + + + + + + + + + {% for entry in leaderboard %} + + + + + + + + + + {% else %} + + {% endfor %} +
RankUserProblemRuntime (s)Memory (KB)Line NumberTimestamp
{{ loop.index }}{{ entry[0] }}{{ problem_titles.get(entry[1], 'Unknown') }}{{ '%.4f'|format(entry[2]) }}{{ entry[3] }}{{ entry[4] if entry[4] else '-' }}{{ entry[5] }}
No leaderboard entries yet.
+
+ + diff --git a/templates/problem.html b/templates/problem.html new file mode 100644 index 0000000..8d725ff --- /dev/null +++ b/templates/problem.html @@ -0,0 +1,72 @@ + + + + + + {{ problem.title }} + + + + +
+ +

{{ problem.title }}

+
+
{{ problem.description }}
+
+

Submit Your Solution (Python)

+
+ + +
+ +
+ +
+
+
+ + + {% if result %} +
+

Result:

+ Runtime: {{ '%.4f'|format(result.runtime) }} seconds
+ Output: +
{{ result.output }}
+ {% if result.error %} + Error: +
{{ result.error }}
+ {% endif %} + {% if result.passed %} +

Passed!

+ {% else %} +

Failed!

+ {% endif %} +
+ {% endif %} + + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..1437d67 --- /dev/null +++ b/utils.py @@ -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