Compare commits
28 Commits
shitdontwo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ae5f12633 | |||
| 1ef6bdfd80 | |||
| f28a6b36ef | |||
| 6c8f34acb9 | |||
| 27f955151a | |||
| f3baec17e4 | |||
| 996cfd2821 | |||
| 2577c96258 | |||
| 9532213adf | |||
| 734ec1dc73 | |||
| e8e1b82d6b | |||
| 8dd5fcbeb7 | |||
| b6ab591054 | |||
| 5dc45b9a9b | |||
| 57a7b0e68f | |||
| 68b7b81741 | |||
| e97dde65fb | |||
| 6079813e2c | |||
| 04dc638cf0 | |||
| 38c3256f19 | |||
| 1374cb9cb1 | |||
| c1ef310f6a | |||
| 525297f19b | |||
| 89ea87951e | |||
| c7c1b8ecd6 | |||
| 0bffdf612c | |||
| a03f9ddb14 | |||
| 3f1f709f30 |
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Detect Markdown files
|
||||
*.md linguist-language=Markdown
|
||||
|
||||
# Ignore CSS files for language detection
|
||||
*.css linguist-vendored
|
||||
|
||||
# Some LF stuff so that linux works ; windows can fuck off
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.gif binary
|
||||
*.zip binary
|
||||
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()
|
||||
@@ -1 +0,0 @@
|
||||
this is a easy sorting problem **it is solvable in less than 2 seconds**
|
||||
@@ -1,17 +0,0 @@
|
||||
import unittest
|
||||
|
||||
# This is the function the user is expected to write.
|
||||
# Its a really simple one, the user can choose not to type tho.
|
||||
|
||||
#def sortlist(lst = [4,3,2,1]) -> list:
|
||||
#return sorted(lst)
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_sort(self):
|
||||
# define x as a empty array.
|
||||
# this will be used for the functiun ; a empty var does not work.
|
||||
self.x = []
|
||||
self.assertEqual(sortlist(self.x), sorted(self.x)) # pyright: ignore[reportUndefinedVariable]
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
902
qtc.py
Normal file
902
qtc.py
Normal file
@@ -0,0 +1,902 @@
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
|
||||
QLabel, QLineEdit, QComboBox, QTextEdit, QPushButton, QStatusBar,
|
||||
QMessageBox, QDialog, QListWidget, QSplitter, QFrame, QSizePolicy,
|
||||
QScrollArea, QFileDialog, QToolTip
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QSize, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QFont, QTextOption, QSyntaxHighlighter, QTextCharFormat, QColor, QTextCursor, QKeyEvent, QTextDocument
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger("ProblemCreator")
|
||||
|
||||
# Optional imports with proper error handling
|
||||
OPTIONAL_DEPENDENCIES = {}
|
||||
|
||||
try:
|
||||
import markdown
|
||||
OPTIONAL_DEPENDENCIES['markdown'] = True
|
||||
except ImportError:
|
||||
logger.warning("Markdown library not available, using basic conversion")
|
||||
OPTIONAL_DEPENDENCIES['markdown'] = False
|
||||
|
||||
try:
|
||||
from pygments import lex
|
||||
from pygments.lexers import PythonLexer
|
||||
from pygments.styles import get_style_by_name
|
||||
OPTIONAL_DEPENDENCIES['pygments'] = True
|
||||
except ImportError:
|
||||
logger.warning("Pygments not available, syntax highlighting will be disabled")
|
||||
OPTIONAL_DEPENDENCIES['pygments'] = False
|
||||
|
||||
|
||||
class CodeEditor(QTextEdit):
|
||||
"""A code editor with syntax highlighting and advanced auto-indentation."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFont(QFont("Consolas", 10))
|
||||
self.indent_width = 4
|
||||
self.setTabStopDistance(QFont("Consolas", 10).pointSize() * self.indent_width)
|
||||
|
||||
# Setup syntax highlighting if available
|
||||
if OPTIONAL_DEPENDENCIES.get('pygments', False):
|
||||
self.highlighter = PythonHighlighter(self.document())
|
||||
|
||||
# Tips for test case editing
|
||||
self.tips = [
|
||||
"💡 Tip: Use descriptive test method names like test_empty_input()",
|
||||
"💡 Tip: Test edge cases like empty inputs, large inputs, and invalid inputs",
|
||||
"💡 Tip: Use assertEqual for exact matches, assertTrue/False for boolean checks",
|
||||
"💡 Tip: Test both valid and invalid inputs to ensure robustness",
|
||||
"💡 Tip: Consider using setUp() method for common test setup",
|
||||
"💡 Tip: Use parameterized tests if you have many similar test cases",
|
||||
"💡 Tip: Make sure your tests are independent of each other",
|
||||
"💡 Tip: Test not only for correct outputs but also for proper error handling"
|
||||
]
|
||||
self.current_tip_index = 0
|
||||
self.tip_timer = QTimer(self)
|
||||
self.tip_timer.timeout.connect(self.show_next_tip)
|
||||
self.tip_timer.start(10000) # Show a new tip every 10 seconds
|
||||
|
||||
def show_next_tip(self):
|
||||
"""Show the next tip in the status bar."""
|
||||
if self.parent() and hasattr(self.parent().parent().parent().parent(), 'statusBar'):
|
||||
status_bar = self.parent().parent().parent().parent().statusBar()
|
||||
if status_bar:
|
||||
tip = self.tips[self.current_tip_index]
|
||||
status_bar.showMessage(tip)
|
||||
self.current_tip_index = (self.current_tip_index + 1) % len(self.tips)
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent):
|
||||
"""Handle key press events for auto-indentation and pairing."""
|
||||
key = event.key()
|
||||
modifiers = event.modifiers()
|
||||
cursor = self.textCursor()
|
||||
|
||||
# Tab key
|
||||
if key == Qt.Key.Key_Tab:
|
||||
if cursor.hasSelection():
|
||||
self.indentSelection()
|
||||
else:
|
||||
# Insert spaces
|
||||
cursor.insertText(" " * self.indent_width)
|
||||
return
|
||||
|
||||
# Shift+Tab key
|
||||
elif key == Qt.Key.Key_Backtab:
|
||||
if cursor.hasSelection():
|
||||
self.dedentSelection()
|
||||
else:
|
||||
self.dedentLine()
|
||||
return
|
||||
|
||||
# Return key
|
||||
elif key == Qt.Key.Key_Return:
|
||||
# Get current line
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
|
||||
line_text = cursor.selectedText()
|
||||
|
||||
# Calculate indentation
|
||||
indent = len(line_text) - len(line_text.lstrip())
|
||||
|
||||
# Check if line ends with colon
|
||||
ends_with_colon = line_text.rstrip().endswith(':')
|
||||
|
||||
# Insert newline with indentation
|
||||
cursor = self.textCursor()
|
||||
cursor.insertText("\n" + " " * indent)
|
||||
|
||||
# Add extra indentation if line ended with colon
|
||||
if ends_with_colon:
|
||||
cursor.insertText(" " * self.indent_width)
|
||||
return
|
||||
|
||||
# Auto-pairing
|
||||
elif key == Qt.Key.Key_ParenLeft:
|
||||
cursor.insertText("()")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_BracketLeft:
|
||||
cursor.insertText("[]")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_BraceLeft:
|
||||
cursor.insertText("{}")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_QuoteDbl:
|
||||
cursor.insertText('""')
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_Apostrophe:
|
||||
cursor.insertText("''")
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Left)
|
||||
self.setTextCursor(cursor)
|
||||
return
|
||||
elif key == Qt.Key.Key_Colon and modifiers == Qt.KeyboardModifier.NoModifier:
|
||||
# Check if we're at the end of the line
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine)
|
||||
if self.textCursor().position() == cursor.position():
|
||||
cursor.insertText(":")
|
||||
return
|
||||
|
||||
# Default behavior
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def indentSelection(self):
|
||||
"""Indent all selected lines."""
|
||||
cursor = self.textCursor()
|
||||
start = cursor.selectionStart()
|
||||
end = cursor.selectionEnd()
|
||||
|
||||
# Move to start of selection
|
||||
cursor.setPosition(start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
|
||||
# Indent each line in selection
|
||||
while cursor.position() <= end:
|
||||
cursor.insertText(" " * self.indent_width)
|
||||
end += self.indent_width
|
||||
if not cursor.movePosition(QTextCursor.MoveOperation.Down):
|
||||
break
|
||||
|
||||
# Restore selection
|
||||
cursor.setPosition(start)
|
||||
cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def dedentSelection(self):
|
||||
"""Dedent all selected lines."""
|
||||
cursor = self.textCursor()
|
||||
start = cursor.selectionStart()
|
||||
end = cursor.selectionEnd()
|
||||
|
||||
# Move to start of selection
|
||||
cursor.setPosition(start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
|
||||
# Dedent each line in selection
|
||||
while cursor.position() <= end:
|
||||
# Check for spaces at beginning of line
|
||||
line_start = cursor.position()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
|
||||
line_text = cursor.selectedText()
|
||||
|
||||
# Count leading spaces
|
||||
leading_spaces = min(len(line_text) - len(line_text.lstrip()), self.indent_width)
|
||||
|
||||
if leading_spaces > 0:
|
||||
# Remove leading spaces
|
||||
cursor.setPosition(line_start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, leading_spaces)
|
||||
cursor.removeSelectedText()
|
||||
end -= leading_spaces
|
||||
|
||||
if not cursor.movePosition(QTextCursor.MoveOperation.Down):
|
||||
break
|
||||
|
||||
# Restore selection
|
||||
cursor.setPosition(max(0, start - self.indent_width))
|
||||
cursor.setPosition(max(0, end - self.indent_width), QTextCursor.MoveMode.KeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def dedentLine(self):
|
||||
"""Dedent the current line."""
|
||||
cursor = self.textCursor()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
|
||||
|
||||
# Check for spaces at beginning of line
|
||||
line_start = cursor.position()
|
||||
cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
|
||||
line_text = cursor.selectedText()
|
||||
|
||||
# Count leading spaces
|
||||
leading_spaces = min(len(line_text) - len(line_text.lstrip()), self.indent_width)
|
||||
|
||||
if leading_spaces > 0:
|
||||
# Remove leading spaces
|
||||
cursor.setPosition(line_start)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, leading_spaces)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
|
||||
class PythonHighlighter(QSyntaxHighlighter):
|
||||
"""Syntax highlighter for Python code using Pygments."""
|
||||
|
||||
def __init__(self, document):
|
||||
super().__init__(document)
|
||||
self._setup_formats()
|
||||
|
||||
def _setup_formats(self):
|
||||
"""Setup text formats for different token types."""
|
||||
self.formats = {}
|
||||
|
||||
# Define syntax highlighting formats
|
||||
keyword_format = QTextCharFormat()
|
||||
keyword_format.setForeground(QColor("#0000FF"))
|
||||
keyword_format.setFontWeight(QFont.Weight.Bold)
|
||||
self.formats['keyword'] = keyword_format
|
||||
|
||||
string_format = QTextCharFormat()
|
||||
string_format.setForeground(QColor("#008000"))
|
||||
self.formats['string'] = string_format
|
||||
|
||||
comment_format = QTextCharFormat()
|
||||
comment_format.setForeground(QColor("#808080"))
|
||||
comment_format.setFontItalic(True)
|
||||
self.formats['comment'] = comment_format
|
||||
|
||||
function_format = QTextCharFormat()
|
||||
function_format.setForeground(QColor("#000080"))
|
||||
function_format.setFontWeight(QFont.Weight.Bold)
|
||||
self.formats['function'] = function_format
|
||||
|
||||
number_format = QTextCharFormat()
|
||||
number_format.setForeground(QColor("#FF8C00"))
|
||||
self.formats['number'] = number_format
|
||||
|
||||
# Python keywords
|
||||
self.keywords = [
|
||||
'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del',
|
||||
'elif', 'else', 'except', 'False', 'finally', 'for', 'from', 'global',
|
||||
'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not', 'or',
|
||||
'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield'
|
||||
]
|
||||
|
||||
# unittest keywords
|
||||
self.unittest_keywords = [
|
||||
'TestCase', 'setUp', 'tearDown', 'setUpClass', 'tearDownClass',
|
||||
'assertEqual', 'assertTrue', 'assertFalse', 'assertRaises',
|
||||
'assertAlmostEqual', 'assertNotEqual', 'assertIn', 'assertNotIn',
|
||||
'assertIs', 'assertIsNot', 'assertIsNone', 'assertIsNotNone',
|
||||
'assertIsInstance', 'assertNotIsInstance', 'assertDictEqual',
|
||||
'assertListEqual', 'assertTupleEqual', 'assertSetEqual',
|
||||
'assertSequenceEqual', 'assertMultiLineEqual', 'assertGreater',
|
||||
'assertGreaterEqual', 'assertLess', 'assertLessEqual', 'assertRegex',
|
||||
'assertNotRegex', 'assertCountEqual'
|
||||
]
|
||||
|
||||
def highlightBlock(self, text):
|
||||
"""Apply syntax highlighting to the current text block."""
|
||||
# Check if we should use pygments
|
||||
if OPTIONAL_DEPENDENCIES.get('pygments', False):
|
||||
self._highlight_with_pygments(text)
|
||||
else:
|
||||
self._highlight_with_basic_rules(text)
|
||||
|
||||
def _highlight_with_pygments(self, text):
|
||||
"""Use pygments for syntax highlighting if available."""
|
||||
try:
|
||||
# Get the text from the current block
|
||||
block = self.currentBlock()
|
||||
start_pos = block.position()
|
||||
end_pos = start_pos + len(text)
|
||||
full_text = self.document().toPlainText()
|
||||
|
||||
# Lex the code and apply formats
|
||||
for token, value in lex(full_text, PythonLexer()):
|
||||
token_str = str(token)
|
||||
token_start = full_text.find(value, start_pos)
|
||||
|
||||
# Skip if token is not in current block
|
||||
if token_start < start_pos or token_start >= end_pos:
|
||||
continue
|
||||
|
||||
# Calculate length within current block
|
||||
token_len = min(len(value), end_pos - token_start)
|
||||
|
||||
# Apply appropriate format
|
||||
if 'Keyword' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['keyword'])
|
||||
elif 'String' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['string'])
|
||||
elif 'Comment' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['comment'])
|
||||
elif 'Name' in token_str and 'Function' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['function'])
|
||||
elif 'Number' in token_str:
|
||||
self.setFormat(token_start - start_pos, token_len, self.formats['number'])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during pygments highlighting: {e}")
|
||||
# Fall back to basic highlighting
|
||||
self._highlight_with_basic_rules(text)
|
||||
|
||||
def _highlight_with_basic_rules(self, text):
|
||||
"""Use basic rules for syntax highlighting."""
|
||||
# Highlight keywords
|
||||
for keyword in self.keywords + self.unittest_keywords:
|
||||
pattern = r'\b' + keyword + r'\b'
|
||||
index = 0
|
||||
while index < len(text):
|
||||
index = text.find(keyword, index)
|
||||
if index == -1:
|
||||
break
|
||||
|
||||
# Check if it's really a word (not part of another word)
|
||||
if (index == 0 or not text[index-1].isalnum()) and \
|
||||
(index + len(keyword) >= len(text) or not text[index + len(keyword)].isalnum()):
|
||||
if keyword in self.keywords:
|
||||
self.setFormat(index, len(keyword), self.formats['keyword'])
|
||||
else:
|
||||
self.setFormat(index, len(keyword), self.formats['function'])
|
||||
|
||||
index += len(keyword)
|
||||
|
||||
# Highlight strings
|
||||
import re
|
||||
string_pattern = re.compile(r'(\".*?\")|(\'.*?\')')
|
||||
for match in string_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, self.formats['string'])
|
||||
|
||||
# Highlight comments
|
||||
comment_pattern = re.compile(r'#.*')
|
||||
for match in comment_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, self.formats['comment'])
|
||||
|
||||
# Highlight numbers
|
||||
number_pattern = re.compile(r'\b\d+\b')
|
||||
for match in number_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, self.formats['number'])
|
||||
|
||||
|
||||
class MarkdownEditor(QWidget):
|
||||
"""A markdown editor with live preview."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Create split view
|
||||
self.splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.splitter)
|
||||
|
||||
# Left side - text editor
|
||||
self.editor = QTextEdit()
|
||||
self.editor.setFont(QFont("Consolas", 10))
|
||||
self.editor.textChanged.connect(self.update_preview)
|
||||
self.splitter.addWidget(self.editor)
|
||||
|
||||
# Right side - preview
|
||||
self.preview = QWebEngineView()
|
||||
self.splitter.addWidget(self.preview)
|
||||
|
||||
# Set initial sizes
|
||||
self.splitter.setSizes([400, 400])
|
||||
|
||||
def update_preview(self):
|
||||
"""Update the markdown preview."""
|
||||
# Get the markdown text
|
||||
markdown_text = self.editor.toPlainText()
|
||||
|
||||
# Convert to HTML
|
||||
html_content = self._markdown_to_html(markdown_text)
|
||||
|
||||
# Update the preview
|
||||
self.preview.setHtml(html_content)
|
||||
|
||||
def _markdown_to_html(self, text):
|
||||
"""Convert markdown text to HTML."""
|
||||
if OPTIONAL_DEPENDENCIES.get('markdown', False):
|
||||
# Use the markdown library if available
|
||||
html = markdown.markdown(text)
|
||||
else:
|
||||
# Fallback to basic conversion
|
||||
html = text
|
||||
html = html.replace("# ", "<h1>").replace("\n# ", "</h1>\n<h1>") + "</h1>"
|
||||
html = html.replace("## ", "<h2>").replace("\n## ", "</h2>\n<h2>") + "</h2>"
|
||||
html = html.replace("### ", "<h3>").replace("\n### ", "</h3>\n<h3>") + "</h3>"
|
||||
html = html.replace("**", "<strong>").replace("**", "</strong>")
|
||||
html = html.replace("*", "<em>").replace("*", "</em>")
|
||||
html = html.replace("`", "<code>").replace("`", "</code>")
|
||||
html = html.replace("\n", "<br>")
|
||||
|
||||
# Wrap in proper HTML structure
|
||||
return f"""
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}}
|
||||
code {{
|
||||
background-color: #f6f8fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', monospace;
|
||||
}}
|
||||
pre {{
|
||||
background-color: #f6f8fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
}}
|
||||
pre code {{
|
||||
background: none;
|
||||
padding: 0;
|
||||
}}
|
||||
blockquote {{
|
||||
border-left: 4px solid #ddd;
|
||||
margin-left: 0;
|
||||
padding-left: 16px;
|
||||
color: #666;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def setPlainText(self, text):
|
||||
"""Set the editor text."""
|
||||
self.editor.setPlainText(text)
|
||||
|
||||
def toPlainText(self):
|
||||
"""Get the editor text."""
|
||||
return self.editor.toPlainText()
|
||||
|
||||
|
||||
class LoadProblemDialog(QDialog):
|
||||
"""Dialog for loading existing problems."""
|
||||
|
||||
def __init__(self, problems, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Load Existing Problem")
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(400, 300)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
label = QLabel("Select a problem to load:")
|
||||
label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
layout.addWidget(label)
|
||||
|
||||
self.list_widget = QListWidget()
|
||||
self.list_widget.addItems(sorted(problems))
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.load_button = QPushButton("Load")
|
||||
self.load_button.clicked.connect(self.accept)
|
||||
button_layout.addWidget(self.load_button)
|
||||
|
||||
self.cancel_button = QPushButton("Cancel")
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def selected_problem(self):
|
||||
"""Get the selected problem name."""
|
||||
items = self.list_widget.selectedItems()
|
||||
return items[0].text() if items else None
|
||||
|
||||
|
||||
class ProblemCreatorApp(QMainWindow):
|
||||
"""Main application for creating coding problems."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Coding Problem Creator")
|
||||
self.setGeometry(100, 100, 1200, 900)
|
||||
|
||||
# Set default paths
|
||||
self.base_path = Path("src/problems")
|
||||
|
||||
# Initialize UI
|
||||
self.create_widgets()
|
||||
self.statusBar().showMessage("Ready to create a new problem...")
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create all UI widgets."""
|
||||
# Central widget
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(central_widget)
|
||||
|
||||
# Create tab widget
|
||||
self.tab_widget = QTabWidget()
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
# Problem Info tab
|
||||
self.info_tab = QWidget()
|
||||
self.tab_widget.addTab(self.info_tab, "Problem Info")
|
||||
self.create_info_tab()
|
||||
|
||||
# Markdown Description tab
|
||||
self.markdown_tab = QWidget()
|
||||
self.tab_widget.addTab(self.markdown_tab, "Markdown Description")
|
||||
self.create_markdown_tab()
|
||||
|
||||
# Test Code tab
|
||||
self.test_tab = QWidget()
|
||||
self.tab_widget.addTab(self.test_tab, "Test Code")
|
||||
self.create_test_tab()
|
||||
|
||||
# Buttons at the bottom
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.create_button = QPushButton("Create Problem")
|
||||
self.create_button.clicked.connect(self.create_problem)
|
||||
button_layout.addWidget(self.create_button)
|
||||
|
||||
self.clear_button = QPushButton("Clear All")
|
||||
self.clear_button.clicked.connect(self.clear_all)
|
||||
button_layout.addWidget(self.clear_button)
|
||||
|
||||
self.load_button = QPushButton("Load Existing")
|
||||
self.load_button.clicked.connect(self.load_existing)
|
||||
button_layout.addWidget(self.load_button)
|
||||
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
def create_info_tab(self):
|
||||
"""Create the Problem Info tab."""
|
||||
layout = QVBoxLayout(self.info_tab)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("Coding Problem Creator")
|
||||
title_font = QFont("Arial", 16, QFont.Weight.Bold)
|
||||
title_label.setFont(title_font)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Problem Name
|
||||
name_layout = QHBoxLayout()
|
||||
name_label = QLabel("Problem Name:")
|
||||
name_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
name_layout.addWidget(name_label)
|
||||
|
||||
self.problem_name = QLineEdit()
|
||||
self.problem_name.setFont(QFont("Arial", 10))
|
||||
name_layout.addWidget(self.problem_name)
|
||||
layout.addLayout(name_layout)
|
||||
|
||||
# Difficulty
|
||||
difficulty_layout = QHBoxLayout()
|
||||
difficulty_label = QLabel("Difficulty:")
|
||||
difficulty_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
difficulty_layout.addWidget(difficulty_label)
|
||||
|
||||
self.difficulty = QComboBox()
|
||||
self.difficulty.addItems(["easy", "medium", "hard"])
|
||||
self.difficulty.setCurrentText("medium")
|
||||
difficulty_layout.addWidget(self.difficulty)
|
||||
difficulty_layout.addStretch()
|
||||
layout.addLayout(difficulty_layout)
|
||||
|
||||
# Plain Text Description
|
||||
desc_label = QLabel("Plain Text Description:")
|
||||
desc_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
self.description_text = QTextEdit()
|
||||
self.description_text.setFont(QFont("Arial", 10))
|
||||
self.description_text.setAcceptRichText(False)
|
||||
layout.addWidget(self.description_text)
|
||||
|
||||
def create_markdown_tab(self):
|
||||
"""Create the Markdown Description tab."""
|
||||
layout = QVBoxLayout(self.markdown_tab)
|
||||
self.description_editor = MarkdownEditor()
|
||||
layout.addWidget(self.description_editor)
|
||||
|
||||
def create_test_tab(self):
|
||||
"""Create the Test Code tab."""
|
||||
layout = QVBoxLayout(self.test_tab)
|
||||
|
||||
# Add tips label
|
||||
tips_label = QLabel("💡 Tips for writing good test cases will appear in the status bar")
|
||||
tips_label.setFont(QFont("Arial", 9))
|
||||
tips_label.setStyleSheet("color: #666; padding: 5px;")
|
||||
layout.addWidget(tips_label)
|
||||
|
||||
self.test_code_editor = CodeEditor()
|
||||
layout.addWidget(self.test_code_editor)
|
||||
|
||||
# Insert template code
|
||||
self._insert_template_code()
|
||||
|
||||
def _insert_template_code(self):
|
||||
"""Insert template test code into the editor."""
|
||||
template_code = '''import unittest
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_example_case(self):
|
||||
"""
|
||||
Test the provided example case.
|
||||
"""
|
||||
solution = Solution()
|
||||
result = solution.solve("input")
|
||||
self.assertEqual(result, "expected_output")
|
||||
|
||||
def test_edge_case_empty_input(self):
|
||||
"""
|
||||
Test with empty input.
|
||||
"""
|
||||
solution = Solution()
|
||||
result = solution.solve("")
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_edge_case_large_input(self):
|
||||
"""
|
||||
Test with a large input to check performance.
|
||||
"""
|
||||
solution = Solution()
|
||||
large_input = "a" * 1000
|
||||
result = solution.solve(large_input)
|
||||
self.assertTrue(result) # Adjust based on expected behavior
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
'''
|
||||
self.test_code_editor.setPlainText(template_code)
|
||||
|
||||
def validate_inputs(self):
|
||||
"""Validate all form inputs."""
|
||||
if not self.problem_name.text().strip():
|
||||
QMessageBox.critical(self, "Error", "Problem name is required!")
|
||||
return False
|
||||
|
||||
if not self.description_text.toPlainText().strip():
|
||||
QMessageBox.critical(self, "Error", "Plain text description is required!")
|
||||
return False
|
||||
|
||||
if not self.description_editor.toPlainText().strip():
|
||||
QMessageBox.critical(self, "Error", "Markdown description is required!")
|
||||
return False
|
||||
|
||||
test_code = self.test_code_editor.toPlainText().strip()
|
||||
if not test_code or "pass" in test_code and len(test_code) < 100:
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm",
|
||||
"The test code seems minimal. Are you sure you want to proceed?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.No:
|
||||
return False
|
||||
|
||||
# Validate problem name (should be filesystem-safe)
|
||||
name = self.problem_name.text().strip()
|
||||
if not name.replace("_", "").replace("-", "").replace(" ", "").isalnum():
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Error",
|
||||
"Problem name should only contain letters, numbers, spaces, hyphens, and underscores!"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_problem(self):
|
||||
"""Create a new problem from the form data."""
|
||||
if not self.validate_inputs():
|
||||
return
|
||||
|
||||
try:
|
||||
# Get values
|
||||
problem_name = self.problem_name.text().strip()
|
||||
description_text = self.description_text.toPlainText().strip() # Plain text
|
||||
description_md = self.description_editor.toPlainText().strip() # Markdown
|
||||
difficulty = self.difficulty.currentText()
|
||||
test_code = self.test_code_editor.toPlainText().strip()
|
||||
|
||||
# Create safe folder name (replace spaces with underscores)
|
||||
folder_name = problem_name.replace(" ", "_").lower()
|
||||
|
||||
# Create directory structure
|
||||
problem_path = self.base_path / folder_name
|
||||
|
||||
# Create directories if they don't exist
|
||||
problem_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create manifest.json - Include both description fields
|
||||
manifest = {
|
||||
"title": problem_name,
|
||||
"description": description_text, # Plain text description
|
||||
"description_md": f"problems/{folder_name}/description.md", # Markdown file path
|
||||
"test_code": f"problems/{folder_name}/test.py",
|
||||
"difficulty": difficulty
|
||||
}
|
||||
|
||||
manifest_path = problem_path / "manifest.json"
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(manifest, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# Create description.md
|
||||
description_md_path = problem_path / "description.md"
|
||||
with open(description_md_path, 'w', encoding='utf-8') as f:
|
||||
f.write(description_md)
|
||||
|
||||
# Create test.py
|
||||
test_py_path = problem_path / "test.py"
|
||||
with open(test_py_path, 'w', encoding='utf-8') as f:
|
||||
f.write(test_code)
|
||||
|
||||
self.statusBar().showMessage(f"✓ Problem '{problem_name}' created successfully in {problem_path}")
|
||||
logger.info(f"Created problem: {problem_name} at {problem_path}")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Success",
|
||||
f"Problem '{problem_name}' created successfully!\n\n"
|
||||
f"Location: {problem_path}\n\n"
|
||||
"Would you like to open the folder?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.open_folder(problem_path)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error creating problem: {str(e)}"
|
||||
self.statusBar().showMessage(error_msg)
|
||||
logger.error(error_msg)
|
||||
QMessageBox.critical(self, "Error", error_msg)
|
||||
|
||||
def open_folder(self, path):
|
||||
"""Cross-platform folder opening."""
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
os.startfile(path)
|
||||
elif sys.platform == "darwin": # macOS
|
||||
os.system(f"open '{path}'")
|
||||
else: # Linux and other Unix-like
|
||||
os.system(f"xdg-open '{path}'")
|
||||
except Exception as e:
|
||||
error_msg = f"Could not open folder: {str(e)}"
|
||||
logger.warning(error_msg)
|
||||
QMessageBox.warning(self, "Warning", error_msg)
|
||||
|
||||
def clear_all(self):
|
||||
"""Clear all form fields."""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm",
|
||||
"Clear all fields?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.problem_name.clear()
|
||||
self.description_text.clear()
|
||||
self.description_editor.setPlainText("")
|
||||
self.difficulty.setCurrentText("medium")
|
||||
self.test_code_editor.clear()
|
||||
|
||||
# Re-insert template
|
||||
self._insert_template_code()
|
||||
|
||||
self.statusBar().showMessage("All fields cleared.")
|
||||
logger.info("Cleared all form fields")
|
||||
|
||||
def load_existing(self):
|
||||
"""Load an existing problem for editing."""
|
||||
try:
|
||||
if not self.base_path.exists():
|
||||
QMessageBox.warning(self, "Warning", "No problems directory found!")
|
||||
return
|
||||
|
||||
# Get list of existing problems
|
||||
problems = [d.name for d in self.base_path.iterdir() if d.is_dir()]
|
||||
|
||||
if not problems:
|
||||
QMessageBox.information(self, "Info", "No existing problems found!")
|
||||
return
|
||||
|
||||
# Create and show selection dialog
|
||||
dialog = LoadProblemDialog(problems, self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
selected_problem = dialog.selected_problem()
|
||||
if selected_problem:
|
||||
self.load_problem_data(selected_problem)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error loading existing problems: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
QMessageBox.critical(self, "Error", error_msg)
|
||||
|
||||
def load_problem_data(self, problem_name):
|
||||
"""Load problem data into the form."""
|
||||
try:
|
||||
problem_path = self.base_path / problem_name
|
||||
manifest_path = problem_path / "manifest.json"
|
||||
test_path = problem_path / "test.py"
|
||||
desc_path = problem_path / "description.md"
|
||||
|
||||
# Load manifest
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
# Load test code
|
||||
test_code = ""
|
||||
if test_path.exists():
|
||||
with open(test_path, 'r', encoding='utf-8') as f:
|
||||
test_code = f.read()
|
||||
|
||||
# Load markdown description
|
||||
description_md = ""
|
||||
if desc_path.exists():
|
||||
with open(desc_path, 'r', encoding='utf-8') as f:
|
||||
description_md = f.read()
|
||||
|
||||
# Load plain text description from manifest
|
||||
description_text = manifest.get('description', '')
|
||||
|
||||
# Populate fields
|
||||
self.problem_name.setText(manifest["title"])
|
||||
self.description_text.setPlainText(description_text)
|
||||
self.description_editor.setPlainText(description_md)
|
||||
self.difficulty.setCurrentText(manifest["difficulty"])
|
||||
self.test_code_editor.setPlainText(test_code)
|
||||
|
||||
self.statusBar().showMessage(f"Loaded problem: {problem_name}")
|
||||
logger.info(f"Loaded problem: {problem_name}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error loading problem data: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
QMessageBox.critical(self, "Error", error_msg)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main application entry point."""
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Set application style
|
||||
app.setStyle('Fusion')
|
||||
|
||||
window = ProblemCreatorApp()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
179
readme.md
179
readme.md
@@ -1,25 +1,174 @@
|
||||
## under construction
|
||||
# QPP - Quick Problem Platfrom
|
||||
|
||||
### Like LeetCode
|
||||
This is a lightweight, LeetCode-inspired problem-solving platform. You can run the server locally, contribute problems, and write unit tests.
|
||||
|
||||
but more lightweight
|
||||
---
|
||||
|
||||
if you want to contribute write tests like this:
|
||||
## Getting Started
|
||||
|
||||
Run the provided bash/_batch_ script to start the server.
|
||||
|
||||
---
|
||||
|
||||
## File Structure for Problems
|
||||
|
||||
Create a folder inside `/problems/` named after your problem. Each folder **must** contain:
|
||||
|
||||
* `manifest.json` # Dont worry you can change the info anytime to reload
|
||||
* `test.py`
|
||||
* `description.md`
|
||||
|
||||
**Example `manifest.json`:**
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
> This structure is mandatory but ensures the easiest workflow.
|
||||
|
||||
---
|
||||
|
||||
## Writing Problem Descriptions
|
||||
|
||||
* Use **simple and easy-to-understand language**. Avoid overly technical explanations.
|
||||
* Syntax:
|
||||
|
||||
* Normal Markdown
|
||||
* Start headings with `##` (looks cleaner than `#`)
|
||||
* Include cross-links to resources, e.g., [W3Schools](https://www.w3schools.com/) or [Python Docs](https://docs.python.org/3/)
|
||||
* Good formatting is always appreciated
|
||||
|
||||
---
|
||||
|
||||
## Developing & Running Locally
|
||||
|
||||
To run the backend during development:
|
||||
|
||||
```bash
|
||||
python -m flask --app ./src/app.py --host=0.0.0.0 --port=5000
|
||||
```
|
||||
|
||||
For production testing:
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
python -m gunicorn -w 4 -b 0.0.0.0:8000 src.app:app
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
```bat
|
||||
:: Create database folder if missing
|
||||
md .\src\database
|
||||
python -m waitress --listen=0.0.0.0:8000 src.app:app
|
||||
```
|
||||
|
||||
> Ensure all required packages are installed via `requirements.txt`. Python is versatile enough for this small backend.
|
||||
|
||||
---
|
||||
|
||||
### Migrating Legacy Code
|
||||
|
||||
When removing or refactoring legacy code:
|
||||
|
||||
1. Check if the code is used anywhere; if critical, branch into a testing branch first.
|
||||
2. Ensure essential functionality is preserved.
|
||||
|
||||
---
|
||||
|
||||
## Committing Changes
|
||||
|
||||
**WE NEED FRONTED PEOPLE!!**, I have no Idea how that works, please someone ( if they are interested help )
|
||||
|
||||
* Ensure your editor uses **LF** line endings (`\n`) instead of CRLF.
|
||||
* To automatically fix CRLF on commit:
|
||||
|
||||
```bash
|
||||
git config core.autocrlf input
|
||||
git add --renormalize .
|
||||
git commit -m "Major Change"
|
||||
```
|
||||
|
||||
* Recommended workflow:
|
||||
|
||||
1. Fork
|
||||
2. Make changes
|
||||
3. Submit a PR
|
||||
4. Review & merge
|
||||
|
||||
> Using WSL with VS Code for development is recommended for consistent line endings on Windows.
|
||||
|
||||
---
|
||||
|
||||
## Writing Unit Tests
|
||||
|
||||
Follow this convention when writing unittests. **Implement the function first, then write tests.**
|
||||
|
||||
### Example: Phone Number Validation
|
||||
|
||||
**Function (`phone_validation.py`):**
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
def is_valid_phone_number(phone_number: str) -> bool:
|
||||
"""Return True if phone_number matches '123-456-7890' format."""
|
||||
return bool(re.search(r"^(\d{3}-){2}\d{4}$", phone_number))
|
||||
```
|
||||
|
||||
**Unit Test (`test_phone_validation.py`):**
|
||||
|
||||
```python
|
||||
import unittest
|
||||
from phone_validation import is_valid_phone_number
|
||||
|
||||
#<!-- The Function the User needs to write -->
|
||||
def revstring(x):
|
||||
return x[::-1]
|
||||
class TestPhoneNumberRegex(unittest.TestCase):
|
||||
|
||||
#<!-- 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])
|
||||
def test_if_valid(self):
|
||||
test_cases = [
|
||||
("123-456-7890", True),
|
||||
("111-222-3333", True),
|
||||
("abc-def-ghij", False),
|
||||
("1234567890", False),
|
||||
("123-45-67890", False),
|
||||
("12-3456-7890", False),
|
||||
("", False),
|
||||
]
|
||||
print("\nPHONE NUMBER VALIDATION TEST RESULTS")
|
||||
|
||||
for phone, expected in test_cases:
|
||||
try:
|
||||
actual = is_valid_phone_number(phone)
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: '{phone}' -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: '{phone}' -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
```
|
||||
unittest.main(verbosity=2)
|
||||
```
|
||||
|
||||
### ✅ Unit Test Guidelines
|
||||
|
||||
1. **Class Naming:** `Test<FunctionOrModuleName>`
|
||||
2. **Method Naming:** `test_<what_is_being_tested>`
|
||||
3. **Use tuples** `(input, expected_output)` for test cases
|
||||
4. Include **edge cases** (empty strings, wrong formats)
|
||||
5. **Print results** clearly for easier debugging
|
||||
6. **Catch exceptions** and display failing input before raising
|
||||
|
||||
|
||||
### What has changed for ease of use:
|
||||
|
||||
If you want to really easily create or edit programs then you should look at the Qt Programm.
|
||||
It basically acts as a "VsCode" of this platform. After editing / creating i would suggest you look over everything in a serious
|
||||
editor. Its still realtively new.
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Flask>=3.0
|
||||
Flask-SQLAlchemy>=3.1
|
||||
Flask-Caching>=2.3.1
|
||||
Markdown>=3.6
|
||||
MarkupSafe>=2.1
|
||||
watchdog>=4.0
|
||||
gunicorn>=23.0.0
|
||||
waitress>=3.0.2
|
||||
pygments>=2.19.2
|
||||
pyqt6>=6.9.1
|
||||
PyQt6-WebEngine>=6.9.0
|
||||
25
run.bash
Normal file
25
run.bash
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # exit if any command fails
|
||||
|
||||
# Ensure src/database directory exists
|
||||
mkdir -p src/database
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "venv" ]; then
|
||||
python -m venv venv
|
||||
fi
|
||||
source venv/bin/activate
|
||||
|
||||
# Upgrade pip and install dependencies
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Export environment variables
|
||||
export FLASK_APP=src.app
|
||||
export FLASK_ENV=production
|
||||
|
||||
# Run with Gunicorn
|
||||
echo "Starting Flask app with Gunicorn..."
|
||||
exec gunicorn -w 4 -b 0.0.0.0:8000 src.app:app
|
||||
|
||||
3
run.bat
Normal file
3
run.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
:: make db directory and then launch the server
|
||||
md .\src\database
|
||||
python -m waitress --listen=0.0.0.0:8000 src.app:app
|
||||
731
src/JavaScript/script.js
Normal file
731
src/JavaScript/script.js
Normal file
@@ -0,0 +1,731 @@
|
||||
/**
|
||||
* -------------------------------------------------------------------
|
||||
* Please read as a Developer:
|
||||
* @file script.js
|
||||
* @author rattatwinko
|
||||
* @description "This is the JavaScript "frontend" File for the website.
|
||||
* This handleds nearly every frontend logic / interaction
|
||||
* if you want to change this, then you should be cautious.
|
||||
* This is a complete mess. And its too complex to refactor.
|
||||
* Atleast for me.
|
||||
* @license MIT
|
||||
* You can freely modify this file and distribute it as you wish.
|
||||
*
|
||||
* @todo
|
||||
* - [] Refactor the jeriatric piece of shit code.
|
||||
* ------------------------------------------------------------------
|
||||
* This is the stupid fucking JavaScript, i hate this so fucking much
|
||||
* why the fuck does this need to exsits, idk.
|
||||
*
|
||||
* CHANGELOG:
|
||||
* aug18@21:51-> pagination for leaderboard ; and some shit refactoring.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
"use strict";
|
||||
|
||||
// Utility functions
|
||||
const utils = {
|
||||
safeLocalStorage: {
|
||||
getItem(key) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
console.warn("localStorage.getItem failed:", e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setItem(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn("localStorage.setItem failed:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func.apply(this, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function executedFunction(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Dark Mode Manager
|
||||
class DarkModeManager {
|
||||
constructor() {
|
||||
this.darkModeToggle = document.getElementById("darkModeToggle");
|
||||
this.html = document.documentElement;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadSavedPreference();
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
loadSavedPreference() {
|
||||
const savedDarkMode = utils.safeLocalStorage.getItem("darkMode");
|
||||
if (
|
||||
savedDarkMode === "true" ||
|
||||
(savedDarkMode === null &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
this.html.classList.add("dark");
|
||||
}
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
this.darkModeToggle?.addEventListener("click", () => {
|
||||
this.html.classList.toggle("dark");
|
||||
utils.safeLocalStorage.setItem("darkMode", this.html.classList.contains("dark"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Problem Manager
|
||||
class ProblemManager {
|
||||
constructor() {
|
||||
this.problemSearch = document.getElementById("problemSearch");
|
||||
this.problemsContainer = document.getElementById("problemsContainer");
|
||||
this.problemsPagination = document.getElementById("problemsPagination");
|
||||
this.problemsPrevBtn = document.getElementById("problemsPrevBtn");
|
||||
this.problemsNextBtn = document.getElementById("problemsNextBtn");
|
||||
this.problemsPaginationInfo = document.getElementById("problemsPaginationInfo");
|
||||
this.difficultyFilter = document.getElementById("difficultyFilter");
|
||||
this.sortProblems = document.getElementById("sortProblems");
|
||||
|
||||
this.allProblemItems = [];
|
||||
this.filteredProblemItems = [];
|
||||
this.currentPage = 1;
|
||||
this.itemsPerPage = 5;
|
||||
this.problemSort = { column: "alpha", direction: "asc" };
|
||||
this.problemDescriptionPopover = null;
|
||||
this.manifestCache = new Map();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.problemsContainer) return;
|
||||
|
||||
this.initializeProblemItems();
|
||||
this.attachEventListeners();
|
||||
this.injectPopoverCSS();
|
||||
this.attachProblemHoverEvents();
|
||||
}
|
||||
|
||||
initializeProblemItems() {
|
||||
this.allProblemItems = Array.from(
|
||||
this.problemsContainer.querySelectorAll(".problem-item") || []
|
||||
);
|
||||
this.filteredProblemItems = this.allProblemItems.map(this.getProblemData);
|
||||
this.updatePagination();
|
||||
}
|
||||
|
||||
getProblemData = (item) => ({
|
||||
element: item,
|
||||
name: item.dataset.name?.toLowerCase() || "",
|
||||
desc: item.dataset.desc?.toLowerCase() || "",
|
||||
difficulty: item.dataset.difficulty || "",
|
||||
});
|
||||
|
||||
updatePagination() {
|
||||
const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage);
|
||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const endIndex = startIndex + this.itemsPerPage;
|
||||
|
||||
// Hide all items first
|
||||
this.allProblemItems.forEach((item) => {
|
||||
item.style.display = "none";
|
||||
});
|
||||
|
||||
// Show current page items
|
||||
this.filteredProblemItems.slice(startIndex, endIndex).forEach((item) => {
|
||||
item.element.style.display = "";
|
||||
});
|
||||
|
||||
// Update pagination controls
|
||||
if (this.problemsPrevBtn) this.problemsPrevBtn.disabled = this.currentPage <= 1;
|
||||
if (this.problemsNextBtn) this.problemsNextBtn.disabled = this.currentPage >= totalPages;
|
||||
|
||||
if (this.problemsPaginationInfo) {
|
||||
this.problemsPaginationInfo.textContent =
|
||||
totalPages > 0
|
||||
? `Page ${this.currentPage} of ${totalPages}`
|
||||
: "No problems found";
|
||||
}
|
||||
|
||||
this.setupPaginationLayout();
|
||||
}
|
||||
|
||||
setupPaginationLayout() {
|
||||
if (this.problemsPagination) {
|
||||
Object.assign(this.problemsPagination.style, {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
right: "0",
|
||||
bottom: "0",
|
||||
margin: "0 auto",
|
||||
width: "100%",
|
||||
background: "inherit",
|
||||
borderTop: "1px solid var(--border)",
|
||||
padding: "12px 0"
|
||||
});
|
||||
// Style the pagination buttons and info text
|
||||
const prevBtn = this.problemsPagination.querySelector('#problemsPrevBtn');
|
||||
const nextBtn = this.problemsPagination.querySelector('#problemsNextBtn');
|
||||
const infoText = this.problemsPagination.querySelector('#problemsPaginationInfo');
|
||||
if (prevBtn) prevBtn.style.marginRight = '10px';
|
||||
if (nextBtn) nextBtn.style.marginLeft = '10px';
|
||||
if (infoText) infoText.style.marginTop = '2px';
|
||||
this.problemsPagination.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (this.problemsContainer?.parentElement) {
|
||||
Object.assign(this.problemsContainer.parentElement.style, {
|
||||
position: "relative",
|
||||
paddingBottom: "56px"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showProblemDescription = async (item) => {
|
||||
this.hideProblemDescription();
|
||||
|
||||
const folder = item.querySelector('a')?.getAttribute('href')?.split('/').pop();
|
||||
if (!folder) return;
|
||||
|
||||
try {
|
||||
let manifest = this.manifestCache.get(folder);
|
||||
|
||||
if (!manifest) {
|
||||
// Try localStorage cache first
|
||||
const cacheKey = `problem_manifest_${folder}`;
|
||||
const cached = utils.safeLocalStorage.getItem(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
manifest = JSON.parse(cached);
|
||||
this.manifestCache.set(folder, manifest);
|
||||
} else {
|
||||
// Fetch from API
|
||||
const response = await fetch(`/api/problem_manifest/${encodeURIComponent(folder)}`);
|
||||
manifest = response.ok ? await response.json() : { description: 'No description.' };
|
||||
|
||||
this.manifestCache.set(folder, manifest);
|
||||
utils.safeLocalStorage.setItem(cacheKey, JSON.stringify(manifest));
|
||||
}
|
||||
}
|
||||
|
||||
this.createPopover(manifest.description || 'No description.', item);
|
||||
} catch (error) {
|
||||
console.warn("Failed to load problem description:", error);
|
||||
this.createPopover('No description available.', item);
|
||||
}
|
||||
};
|
||||
|
||||
createPopover(description, item) {
|
||||
this.problemDescriptionPopover = document.createElement("div");
|
||||
this.problemDescriptionPopover.className = "problem-desc-popover";
|
||||
this.problemDescriptionPopover.textContent = description;
|
||||
document.body.appendChild(this.problemDescriptionPopover);
|
||||
|
||||
const rect = item.getBoundingClientRect();
|
||||
Object.assign(this.problemDescriptionPopover.style, {
|
||||
position: "fixed",
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
top: `${rect.bottom + window.scrollY + 6}px`,
|
||||
zIndex: "1000",
|
||||
minWidth: `${rect.width}px`
|
||||
});
|
||||
}
|
||||
|
||||
hideProblemDescription = () => {
|
||||
if (this.problemDescriptionPopover) {
|
||||
this.problemDescriptionPopover.remove();
|
||||
this.problemDescriptionPopover = null;
|
||||
}
|
||||
};
|
||||
|
||||
attachProblemHoverEvents() {
|
||||
this.allProblemItems.forEach((item) => {
|
||||
item.addEventListener("mouseenter", () => this.showProblemDescription(item));
|
||||
item.addEventListener("mouseleave", this.hideProblemDescription);
|
||||
item.addEventListener("mousemove", this.handleMouseMove);
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseMove = utils.throttle((e) => {
|
||||
if (this.problemDescriptionPopover) {
|
||||
this.problemDescriptionPopover.style.left = `${e.clientX + 10}px`;
|
||||
}
|
||||
}, 16); // ~60fps
|
||||
|
||||
sortProblemItems(column, direction) {
|
||||
this.filteredProblemItems.sort((a, b) => {
|
||||
let valueA, valueB;
|
||||
|
||||
switch (column) {
|
||||
case "alpha":
|
||||
valueA = a.name;
|
||||
valueB = b.name;
|
||||
break;
|
||||
case "difficulty":
|
||||
const difficultyOrder = { easy: 1, medium: 2, hard: 3 };
|
||||
valueA = difficultyOrder[a.difficulty] || 0;
|
||||
valueB = difficultyOrder[b.difficulty] || 0;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
let comparison = 0;
|
||||
if (typeof valueA === "number" && typeof valueB === "number") {
|
||||
comparison = valueA - valueB;
|
||||
} else {
|
||||
comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
||||
}
|
||||
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
this.problemsPrevBtn?.addEventListener("click", () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.updatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
this.problemsNextBtn?.addEventListener("click", () => {
|
||||
const totalPages = Math.ceil(this.filteredProblemItems.length / this.itemsPerPage);
|
||||
if (this.currentPage < totalPages) {
|
||||
this.currentPage++;
|
||||
this.updatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
this.problemSearch?.addEventListener("input", utils.debounce(() => {
|
||||
this.filterProblems();
|
||||
this.currentPage = 1;
|
||||
this.updatePagination();
|
||||
}, 300));
|
||||
|
||||
this.difficultyFilter?.addEventListener("change", () => {
|
||||
this.filterProblems();
|
||||
this.currentPage = 1;
|
||||
this.updatePagination();
|
||||
});
|
||||
|
||||
this.sortProblems?.addEventListener("change", () => {
|
||||
const value = this.sortProblems.value;
|
||||
if (value === "alpha" || value === "difficulty") {
|
||||
if (this.problemSort.column === value) {
|
||||
this.problemSort.direction = this.problemSort.direction === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
this.problemSort.column = value;
|
||||
this.problemSort.direction = "asc";
|
||||
}
|
||||
this.sortProblemItems(this.problemSort.column, this.problemSort.direction);
|
||||
this.currentPage = 1;
|
||||
this.updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filterProblems() {
|
||||
const searchTerm = (this.problemSearch?.value || "").toLowerCase().trim();
|
||||
const difficulty = this.difficultyFilter?.value || "all";
|
||||
|
||||
this.filteredProblemItems = this.allProblemItems
|
||||
.map(this.getProblemData)
|
||||
.filter(item => {
|
||||
const matchesSearch = !searchTerm ||
|
||||
item.name.includes(searchTerm) ||
|
||||
item.desc.includes(searchTerm);
|
||||
|
||||
const matchesDifficulty = difficulty === "all" ||
|
||||
item.difficulty === difficulty;
|
||||
|
||||
return matchesSearch && matchesDifficulty;
|
||||
});
|
||||
}
|
||||
|
||||
injectPopoverCSS() {
|
||||
if (document.getElementById("problem-desc-popover-style")) return;
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "problem-desc-popover-style";
|
||||
style.textContent = `
|
||||
.problem-desc-popover {
|
||||
background: var(--card, #fff);
|
||||
color: var(--text, #222);
|
||||
border: 1px solid var(--border, #e5e7eb);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(16,24,40,0.13);
|
||||
padding: 12px 16px;
|
||||
font-size: 0.98rem;
|
||||
max-width: 350px;
|
||||
min-width: 180px;
|
||||
pointer-events: none;
|
||||
opacity: 0.97;
|
||||
transition: opacity 0.2s;
|
||||
word-break: break-word;
|
||||
}
|
||||
html.dark .problem-desc-popover {
|
||||
background: var(--card, #1e293b);
|
||||
color: var(--text, #f1f5f9);
|
||||
border: 1px solid var(--border, #334155);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Clean up event listeners and resources
|
||||
this.hideProblemDescription();
|
||||
this.manifestCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Leaderboard Manager
|
||||
class LeaderboardManager {
|
||||
constructor() {
|
||||
this.problemFilter = document.getElementById("problemFilter");
|
||||
this.runtimeFilter = document.getElementById("runtimeFilter");
|
||||
this.leaderboardBody = document.getElementById("leaderboardBody");
|
||||
this.sortableHeaders = document.querySelectorAll(".sortable");
|
||||
this.rankInfoBtn = document.getElementById("rankInfoBtn");
|
||||
this.rankingExplanation = document.getElementById("rankingExplanation");
|
||||
|
||||
this.currentSort = { column: "rank", direction: "asc" };
|
||||
this.allRows = [];
|
||||
this.filteredRows = [];
|
||||
this.currentPage = 1;
|
||||
this.itemsPerPage = 5;
|
||||
this.leaderboardPagination = document.createElement("div");
|
||||
this.leaderboardPagination.className = "pagination-controls";
|
||||
this.leaderboardPagination.style.display = "flex";
|
||||
this.leaderboardPagination.style.justifyContent = "center";
|
||||
this.leaderboardPagination.style.position = "absolute";
|
||||
this.leaderboardPagination.style.left = 0;
|
||||
this.leaderboardPagination.style.right = 0;
|
||||
this.leaderboardPagination.style.bottom = 0;
|
||||
this.leaderboardPagination.style.margin = "0 auto 0 auto";
|
||||
this.leaderboardPagination.style.width = "100%";
|
||||
this.leaderboardPagination.style.background = "inherit";
|
||||
this.leaderboardPagination.style.borderTop = "1px solid var(--border)";
|
||||
this.leaderboardPagination.style.padding = "12px 0";
|
||||
this.leaderboardPagination.innerHTML = `
|
||||
<button class="pagination-btn" id="leaderboardPrevBtn" disabled style="margin-right:10px;">← Previous</button>
|
||||
<span class="pagination-info" id="leaderboardPaginationInfo" style="margin-top:2px;">Page 1 of 1</span>
|
||||
<button class="pagination-btn" id="leaderboardNextBtn" disabled style="margin-left:10px;">Next →</button>
|
||||
`;
|
||||
this.leaderboardPrevBtn = this.leaderboardPagination.querySelector("#leaderboardPrevBtn");
|
||||
this.leaderboardNextBtn = this.leaderboardPagination.querySelector("#leaderboardNextBtn");
|
||||
this.leaderboardPaginationInfo = this.leaderboardPagination.querySelector("#leaderboardPaginationInfo");
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.leaderboardBody || this.leaderboardBody.children.length === 0) return;
|
||||
this.initializeRows();
|
||||
this.attachEventListeners();
|
||||
this.filterLeaderboard();
|
||||
this.setInitialSortIndicator();
|
||||
// Insert pagination controls after leaderboard table
|
||||
const leaderboardContainer = document.getElementById("leaderboardContainer");
|
||||
if (leaderboardContainer && !leaderboardContainer.contains(this.leaderboardPagination)) {
|
||||
leaderboardContainer.appendChild(this.leaderboardPagination);
|
||||
// Ensure parent card is relatively positioned and has enough bottom padding
|
||||
const leaderboardCard = leaderboardContainer.closest('.card');
|
||||
if (leaderboardCard) {
|
||||
leaderboardCard.style.position = "relative";
|
||||
leaderboardCard.style.paddingBottom = "56px";
|
||||
}
|
||||
}
|
||||
// Also ensure the parent card (section.card) contains the controls for correct layout
|
||||
const leaderboardCard = leaderboardContainer?.closest('.card');
|
||||
if (leaderboardCard && !leaderboardCard.contains(this.leaderboardPagination)) {
|
||||
leaderboardCard.appendChild(this.leaderboardPagination);
|
||||
}
|
||||
}
|
||||
|
||||
initializeRows() {
|
||||
this.allRows = Array.from(this.leaderboardBody.querySelectorAll("tr")).map((row, index) => ({
|
||||
element: row,
|
||||
user: row.dataset.user || "",
|
||||
problem: row.dataset.problem || "",
|
||||
runtime: parseFloat(row.dataset.runtime) || 0,
|
||||
memory: parseFloat(row.dataset.memory) || 0,
|
||||
timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(),
|
||||
language: row.dataset.language || "",
|
||||
originalIndex: index,
|
||||
}));
|
||||
}
|
||||
|
||||
filterLeaderboard() {
|
||||
const problemTerm = (this.problemFilter?.value || "").toLowerCase().trim();
|
||||
const runtimeType = this.runtimeFilter?.value || "all";
|
||||
// Filter rows
|
||||
this.filteredRows = this.allRows.filter((rowData) => {
|
||||
let visible = true;
|
||||
if (problemTerm) {
|
||||
visible = rowData.problem.toLowerCase().includes(problemTerm);
|
||||
}
|
||||
return visible;
|
||||
});
|
||||
// Apply runtime filter (best/worst per user per problem)
|
||||
if (runtimeType === "best" || runtimeType === "worst") {
|
||||
const userProblemGroups = {};
|
||||
this.filteredRows.forEach((rowData) => {
|
||||
const key = `${rowData.user}::${rowData.problem}`;
|
||||
if (!userProblemGroups[key]) userProblemGroups[key] = [];
|
||||
userProblemGroups[key].push(rowData);
|
||||
});
|
||||
this.filteredRows = Object.values(userProblemGroups).flatMap((group) => {
|
||||
if (group.length <= 1) return group;
|
||||
group.sort((a, b) => a.runtime - b.runtime);
|
||||
const keepIndex = runtimeType === "best" ? 0 : group.length - 1;
|
||||
return [group[keepIndex]];
|
||||
});
|
||||
}
|
||||
this.currentPage = 1;
|
||||
this.updateLeaderboardPagination();
|
||||
}
|
||||
|
||||
updateLeaderboardPagination() {
|
||||
const totalPages = Math.ceil(this.filteredRows.length / this.itemsPerPage) || 1;
|
||||
if (this.currentPage > totalPages) this.currentPage = totalPages;
|
||||
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const endIndex = startIndex + this.itemsPerPage;
|
||||
// Hide all rows first
|
||||
this.allRows.forEach((rowData) => {
|
||||
rowData.element.style.display = "none";
|
||||
});
|
||||
// Show only current page rows
|
||||
this.filteredRows.slice(startIndex, endIndex).forEach((rowData) => {
|
||||
rowData.element.style.display = "";
|
||||
});
|
||||
// Update pagination controls
|
||||
if (this.leaderboardPrevBtn) this.leaderboardPrevBtn.disabled = this.currentPage <= 1;
|
||||
if (this.leaderboardNextBtn) this.leaderboardNextBtn.disabled = this.currentPage >= totalPages;
|
||||
if (this.leaderboardPaginationInfo) {
|
||||
this.leaderboardPaginationInfo.textContent =
|
||||
totalPages > 0 ? `Page ${this.currentPage} of ${totalPages}` : "No entries found";
|
||||
}
|
||||
// Always show and center pagination at the bottom of the leaderboard card
|
||||
if (this.leaderboardPagination) {
|
||||
this.leaderboardPagination.classList.remove("hidden");
|
||||
this.leaderboardPagination.style.display = "flex";
|
||||
this.leaderboardPagination.style.justifyContent = "center";
|
||||
this.leaderboardPagination.style.position = "absolute";
|
||||
this.leaderboardPagination.style.left = 0;
|
||||
this.leaderboardPagination.style.right = 0;
|
||||
this.leaderboardPagination.style.bottom = 0;
|
||||
this.leaderboardPagination.style.margin = "0 auto 0 auto";
|
||||
this.leaderboardPagination.style.width = "100%";
|
||||
this.leaderboardPagination.style.background = "inherit";
|
||||
this.leaderboardPagination.style.borderTop = "1px solid var(--border)";
|
||||
this.leaderboardPagination.style.padding = "12px 0";
|
||||
}
|
||||
// Make sure the parent leaderboard card is relatively positioned
|
||||
const leaderboardContainer = document.getElementById("leaderboardContainer");
|
||||
if (leaderboardContainer && leaderboardContainer.parentElement) {
|
||||
leaderboardContainer.parentElement.style.position = "relative";
|
||||
leaderboardContainer.parentElement.style.paddingBottom = "56px";
|
||||
}
|
||||
// Recalculate ranks for visible rows
|
||||
this.calculateOverallRanking();
|
||||
}
|
||||
|
||||
calculateOverallRanking() {
|
||||
// Only consider visible rows (current page)
|
||||
const visibleRows = this.filteredRows.slice(
|
||||
(this.currentPage - 1) * this.itemsPerPage,
|
||||
(this.currentPage - 1) * this.itemsPerPage + this.itemsPerPage
|
||||
);
|
||||
if (visibleRows.length === 0) return;
|
||||
// Group submissions by problem to find the best performance for each
|
||||
const problemBests = {};
|
||||
visibleRows.forEach((rowData) => {
|
||||
const problem = rowData.problem;
|
||||
if (!problemBests[problem]) {
|
||||
problemBests[problem] = {
|
||||
bestRuntime: Infinity,
|
||||
bestMemory: Infinity,
|
||||
};
|
||||
}
|
||||
problemBests[problem].bestRuntime = Math.min(
|
||||
problemBests[problem].bestRuntime,
|
||||
rowData.runtime
|
||||
);
|
||||
problemBests[problem].bestMemory = Math.min(
|
||||
problemBests[problem].bestMemory,
|
||||
rowData.memory
|
||||
);
|
||||
});
|
||||
// Calculate normalized scores for each submission
|
||||
visibleRows.forEach((rowData) => {
|
||||
const problemBest = problemBests[rowData.problem];
|
||||
const runtimeScore =
|
||||
problemBest.bestRuntime > 0
|
||||
? rowData.runtime / problemBest.bestRuntime
|
||||
: 1;
|
||||
const memoryScore =
|
||||
problemBest.bestMemory > 0
|
||||
? rowData.memory / problemBest.bestMemory
|
||||
: 1;
|
||||
rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3;
|
||||
});
|
||||
// Sort by overall score (lower is better), then by timestamp (earlier is better for ties)
|
||||
visibleRows.sort((a, b) => {
|
||||
const scoreDiff = a.overallScore - b.overallScore;
|
||||
if (Math.abs(scoreDiff) > 0.000001) return scoreDiff;
|
||||
return a.timestamp - b.timestamp;
|
||||
});
|
||||
// Reorder DOM elements and update ranks
|
||||
const fragment = document.createDocumentFragment();
|
||||
visibleRows.forEach((rowData, index) => {
|
||||
fragment.appendChild(rowData.element);
|
||||
// Update rank cell
|
||||
const rankCell = rowData.element.cells[0];
|
||||
if (rankCell) rankCell.textContent = index + 1 + (this.currentPage - 1) * this.itemsPerPage;
|
||||
// Update rank classes
|
||||
rowData.element.className = rowData.element.className.replace(/\brank-\d+\b/g, "");
|
||||
if (index === 0) rowData.element.classList.add("rank-1");
|
||||
else if (index < 3) rowData.element.classList.add("rank-top3");
|
||||
});
|
||||
this.leaderboardBody.appendChild(fragment);
|
||||
// this.updateRankClasses(); // Function does not exist, so remove this call
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
// Sorting event listeners
|
||||
this.sortableHeaders.forEach((header) => {
|
||||
header.addEventListener("click", () => {
|
||||
const column = header.dataset.sort;
|
||||
if (!column) return;
|
||||
// Remove sorting classes from all headers
|
||||
this.sortableHeaders.forEach((h) => h.classList.remove("sort-asc", "sort-desc"));
|
||||
// Toggle sort direction
|
||||
if (this.currentSort.column === column) {
|
||||
this.currentSort.direction = this.currentSort.direction === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
this.currentSort.column = column;
|
||||
this.currentSort.direction = "asc";
|
||||
}
|
||||
// Add sorting class to current header
|
||||
header.classList.add(`sort-${this.currentSort.direction}`);
|
||||
// Sort filteredRows
|
||||
this.filteredRows.sort((a, b) => {
|
||||
let valueA = a[column];
|
||||
let valueB = b[column];
|
||||
if (typeof valueA === "string") valueA = valueA.toLowerCase();
|
||||
if (typeof valueB === "string") valueB = valueB.toLowerCase();
|
||||
let comparison = 0;
|
||||
if (typeof valueA === "number" && typeof valueB === "number") {
|
||||
comparison = valueA - valueB;
|
||||
} else {
|
||||
comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
||||
}
|
||||
return this.currentSort.direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
this.currentPage = 1;
|
||||
this.updateLeaderboardPagination();
|
||||
});
|
||||
});
|
||||
|
||||
// Filter event listeners with debouncing
|
||||
this.problemFilter?.addEventListener("input", utils.debounce(() => {
|
||||
this.filterLeaderboard();
|
||||
}, 300));
|
||||
this.runtimeFilter?.addEventListener("change", () => this.filterLeaderboard());
|
||||
|
||||
// Pagination event listeners
|
||||
this.leaderboardPrevBtn?.addEventListener("click", () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.updateLeaderboardPagination();
|
||||
}
|
||||
});
|
||||
this.leaderboardNextBtn?.addEventListener("click", () => {
|
||||
const totalPages = Math.ceil(this.filteredRows.length / this.itemsPerPage) || 1;
|
||||
if (this.currentPage < totalPages) {
|
||||
this.currentPage++;
|
||||
this.updateLeaderboardPagination();
|
||||
}
|
||||
});
|
||||
|
||||
// Rank info popout
|
||||
this.rankInfoBtn?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
this.rankingExplanation?.classList.toggle("active");
|
||||
this.rankInfoBtn?.classList.toggle("active");
|
||||
});
|
||||
|
||||
// Close ranking explanation when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (
|
||||
this.rankingExplanation?.classList.contains("active") &&
|
||||
!this.rankingExplanation.contains(e.target) &&
|
||||
!this.rankInfoBtn?.contains(e.target)
|
||||
) {
|
||||
this.rankingExplanation.classList.remove("active");
|
||||
this.rankInfoBtn?.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setInitialSortIndicator() {
|
||||
const defaultHeader = document.querySelector('[data-sort="rank"]');
|
||||
if (defaultHeader) {
|
||||
defaultHeader.classList.add("sort-asc");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all managers
|
||||
const darkModeManager = new DarkModeManager();
|
||||
const problemManager = new ProblemManager();
|
||||
const leaderboardManager = new LeaderboardManager();
|
||||
|
||||
// Apply dark mode to dynamically created elements
|
||||
const applyDarkModeToElements = () => {
|
||||
// Any additional dark mode styling for dynamically created elements can go here
|
||||
};
|
||||
|
||||
// Watch for dark mode changes
|
||||
const darkModeObserver = new MutationObserver(applyDarkModeToElements);
|
||||
darkModeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener("beforeunload", () => {
|
||||
problemManager.destroy();
|
||||
darkModeObserver.disconnect();
|
||||
});
|
||||
});
|
||||
@@ -1,124 +1,166 @@
|
||||
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 os
|
||||
## 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.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
|
||||
# Problems are now loaded from manifests by the background scanner. No need to load problems.json.
|
||||
|
||||
# Start the background thread to scan problems
|
||||
start_problem_scanner()
|
||||
|
||||
@app.route("/script.js")
|
||||
def script():
|
||||
return send_from_directory("templates", "script.js")
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory("templates", "favicon", "favicon.ico")
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
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()
|
||||
# Map folder to title for display
|
||||
problem_titles = {folder: folder.replace('_', ' ').title() for folder, _, _ in problems}
|
||||
return render_template('index.html', problems=problems, leaderboard=leaderboard, problem_titles=problem_titles)
|
||||
|
||||
@app.route('/problem/new', methods=['GET', 'POST'])
|
||||
def new_problem():
|
||||
if request.method == 'POST':
|
||||
title = request.form['title']
|
||||
description = request.form['description']
|
||||
test_code = request.form['test_code']
|
||||
problem = Problem(title=title, description=description, test_code=test_code)
|
||||
db.session.add(problem)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'))
|
||||
return render_template('new_problem.html')
|
||||
|
||||
@app.route('/problem/<folder>', methods=['GET', 'POST'])
|
||||
def view_problem(folder):
|
||||
db_path = Path(__file__).parent / '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
|
||||
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)
|
||||
# Find the highest line number in the AST (for multi-function/user code)
|
||||
def get_max_lineno(node):
|
||||
max_lineno = getattr(node, 'lineno', 0)
|
||||
for child in ast.iter_child_nodes(node):
|
||||
max_lineno = max(max_lineno, get_max_lineno(child))
|
||||
return max_lineno
|
||||
line_number = get_max_lineno(tree)
|
||||
except Exception:
|
||||
pass
|
||||
# If there was an error, try to get the error line number from the traceback
|
||||
if run_result['error']:
|
||||
tb = run_result['error']
|
||||
import traceback
|
||||
try:
|
||||
tb_lines = traceback.extract_tb(traceback.TracebackException.from_string(tb).stack)
|
||||
if tb_lines:
|
||||
line_number = tb_lines[-1].lineno
|
||||
except Exception:
|
||||
pass
|
||||
log_leaderboard(username, problem['folder'], run_result['runtime'], memory_used, line_number)
|
||||
result = run_result
|
||||
return render_template('problem.html', problem=problem, result=result)
|
||||
|
||||
@app.template_filter('markdown')
|
||||
def markdown_filter(text):
|
||||
return Markup(md.markdown(text or '', extensions=['extra', 'sane_lists']))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
# API endpoint to get problem manifest (description) by folder
|
||||
from markupsafe import Markup
|
||||
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, jsonify
|
||||
from flask_caching import Cache
|
||||
import markdown as md
|
||||
import ast
|
||||
from src.models import db, Problem, Solution
|
||||
from src.utils import run_code_against_tests
|
||||
from src.leaderboard import create_leaderboard_table, log_leaderboard, get_leaderboard
|
||||
import os
|
||||
from src.problem_scanner import start_problem_scanner
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
# Config cache
|
||||
config = {
|
||||
"DEBUG": True,
|
||||
"CACHE_TYPE": "SimpleCache",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300
|
||||
}
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_mapping(config)
|
||||
cache = Cache(app)
|
||||
BASE_DIR = Path(__file__).parent
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{BASE_DIR / 'database' / 'db.sqlite3'}"
|
||||
|
||||
print(f"[ INFO ] : Using database URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
@app.before_request
|
||||
def setup():
|
||||
db.create_all()
|
||||
create_leaderboard_table() # Ensure leaderboard table exists
|
||||
# Problems are loaded from manifests by the background scanner ; running on a different thread. No need to load problems.json.
|
||||
|
||||
# Start the background thread to scan problems
|
||||
start_problem_scanner()
|
||||
|
||||
@app.route('/api/problem_manifest/<folder>')
|
||||
def api_problem_manifest(folder):
|
||||
# Try to load manifest.json from the problem folder
|
||||
import json
|
||||
manifest_path = BASE_DIR / 'problems' / folder / 'manifest.json'
|
||||
if not manifest_path.exists():
|
||||
return jsonify({'error': 'Manifest not found'}), 404
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest = json.load(f)
|
||||
return jsonify(manifest)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# I introduce you to the fucking JavaScript shit routes, fuck javascripts
|
||||
@app.route('/JavaScript/<path:filename>')
|
||||
@cache.cached(timeout=300)
|
||||
def serve_js(filename):
|
||||
return send_from_directory('JavaScript', filename)
|
||||
|
||||
@app.route("/script.js")
|
||||
@cache.cached(timeout=300)
|
||||
def script():
|
||||
return send_from_directory("JavaScript", "script.js")
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
@cache.cached()
|
||||
def favicon():
|
||||
return send_from_directory("templates", "favicon.ico")
|
||||
|
||||
@app.route('/')
|
||||
@cache.cached(timeout=300)
|
||||
def index():
|
||||
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
#<!-- The query was fucked up so it fetched the fucking testcode -->
|
||||
c.execute('SELECT folder, description, test_code, difficulty FROM problems')
|
||||
problems = c.fetchall()
|
||||
conn.close()
|
||||
# Get leaderboard entries
|
||||
leaderboard = get_leaderboard()
|
||||
# Map folder to title for display
|
||||
problem_titles = {folder: folder.replace('_', ' ').title() for folder, _, _, _ in problems}
|
||||
return render_template('index.html', problems=problems, leaderboard=leaderboard, problem_titles=problem_titles)
|
||||
|
||||
@app.route('/problem/new', methods=['GET', 'POST'])
|
||||
def new_problem():
|
||||
if request.method == 'POST':
|
||||
title = request.form['title']
|
||||
description = request.form['description']
|
||||
test_code = request.form['test_code']
|
||||
problem = Problem(title=title, description=description, test_code=test_code)
|
||||
db.session.add(problem)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'))
|
||||
return render_template('new_problem.html')
|
||||
|
||||
@app.route('/problem/<folder>', methods=['GET', 'POST'])
|
||||
def view_problem(folder):
|
||||
db_path = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT folder, description,test_code , difficulty FROM problems WHERE folder = ?', (folder,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return 'Problem not found', 404
|
||||
|
||||
problem = {
|
||||
'folder': row[0],
|
||||
'description': row[1],
|
||||
'difficulty': row[3], # now correct
|
||||
'test_code': row[2], # now correct
|
||||
}
|
||||
|
||||
result = None
|
||||
if request.method == 'POST':
|
||||
user_code = request.form['user_code']
|
||||
username = request.form.get('username', '').strip() or 'Anonymous'
|
||||
import tracemalloc
|
||||
tracemalloc.start()
|
||||
run_result = run_code_against_tests(user_code, problem['test_code'])
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
memory_used = peak // 1024 # in KB
|
||||
|
||||
# Try to get the last line number executed (even for successful runs)
|
||||
line_number = None
|
||||
try:
|
||||
tree = ast.parse(user_code)
|
||||
# Find the highest line number in the AST (for multi-function/user code)
|
||||
def get_max_lineno(node):
|
||||
max_lineno = getattr(node, 'lineno', 0)
|
||||
for child in ast.iter_child_nodes(node):
|
||||
max_lineno = max(max_lineno, get_max_lineno(child))
|
||||
return max_lineno
|
||||
line_number = get_max_lineno(tree)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If there was an error, try to get the error line number from the traceback
|
||||
if run_result['error']:
|
||||
tb = run_result['error']
|
||||
import traceback
|
||||
try:
|
||||
tb_lines = traceback.extract_tb(traceback.TracebackException.from_string(tb).stack)
|
||||
if tb_lines:
|
||||
line_number = tb_lines[-1].lineno
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ONLY log to leaderboard if the solution passed all tests
|
||||
if run_result['passed']:
|
||||
log_leaderboard(username, problem['folder'], run_result['runtime'], memory_used, line_number)
|
||||
|
||||
result = run_result
|
||||
return render_template('problem.html', problem=problem, result=result)
|
||||
|
||||
@app.template_filter('markdown')
|
||||
def markdown_filter(text):
|
||||
return Markup(md.markdown(text or '', extensions=['extra', 'sane_lists']))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
222
src/cache.py
Normal file
222
src/cache.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
High-performance in-memory caching module with LRU eviction policy.
|
||||
"""
|
||||
import time
|
||||
from typing import Any, Callable, Optional, Dict, List, Tuple
|
||||
import threading
|
||||
import functools
|
||||
import logging
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CacheEntry:
|
||||
"""Represents a single cache entry with metadata."""
|
||||
|
||||
__slots__ = ('value', 'timestamp', 'expires_at', 'hits')
|
||||
|
||||
def __init__(self, value: Any, timeout: int):
|
||||
self.value = value
|
||||
self.timestamp = time.time()
|
||||
self.expires_at = self.timestamp + timeout
|
||||
self.hits = 0
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if the cache entry has expired."""
|
||||
return time.time() >= self.expires_at
|
||||
|
||||
def hit(self) -> None:
|
||||
"""Increment the hit counter."""
|
||||
self.hits += 1
|
||||
|
||||
class FastMemoryCache:
|
||||
"""
|
||||
High-performance in-memory cache with LRU eviction policy.
|
||||
Thread-safe and optimized for frequent reads.
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int = 1000, default_timeout: int = 300):
|
||||
"""
|
||||
Initialize the cache.
|
||||
|
||||
Args:
|
||||
max_size: Maximum number of items to store in cache
|
||||
default_timeout: Default expiration time in seconds
|
||||
"""
|
||||
self.max_size = max_size
|
||||
self.default_timeout = default_timeout
|
||||
self._cache: Dict[str, CacheEntry] = {}
|
||||
self._lock = threading.RLock()
|
||||
self._hits = 0
|
||||
self._misses = 0
|
||||
self._evictions = 0
|
||||
|
||||
# Start background cleaner thread
|
||||
self._cleaner_thread = threading.Thread(target=self._clean_expired, daemon=True)
|
||||
self._cleaner_thread.start()
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""
|
||||
Get a value from the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
|
||||
Returns:
|
||||
Cached value or None if not found/expired
|
||||
"""
|
||||
with self._lock:
|
||||
entry = self._cache.get(key)
|
||||
|
||||
if entry is None:
|
||||
self._misses += 1
|
||||
return None
|
||||
|
||||
if entry.is_expired():
|
||||
del self._cache[key]
|
||||
self._misses += 1
|
||||
self._evictions += 1
|
||||
return None
|
||||
|
||||
entry.hit()
|
||||
self._hits += 1
|
||||
return entry.value
|
||||
|
||||
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> None:
|
||||
"""
|
||||
Set a value in the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
value: Value to cache
|
||||
timeout: Optional timeout in seconds (uses default if None)
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
|
||||
with self._lock:
|
||||
# Evict if cache is full (LRU policy)
|
||||
if len(self._cache) >= self.max_size and key not in self._cache:
|
||||
self._evict_lru()
|
||||
|
||||
self._cache[key] = CacheEntry(value, timeout)
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""
|
||||
Delete a key from the cache.
|
||||
|
||||
Args:
|
||||
key: Cache key to delete
|
||||
|
||||
Returns:
|
||||
True if key was deleted, False if not found
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
self._evictions += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all items from the cache."""
|
||||
with self._lock:
|
||||
self._cache.clear()
|
||||
self._evictions += len(self._cache)
|
||||
|
||||
def _evict_lru(self) -> None:
|
||||
"""Evict the least recently used item from the cache."""
|
||||
if not self._cache:
|
||||
return
|
||||
|
||||
# Find the entry with the fewest hits (simplified LRU)
|
||||
lru_key = min(self._cache.keys(), key=lambda k: self._cache[k].hits)
|
||||
del self._cache[lru_key]
|
||||
self._evictions += 1
|
||||
|
||||
def _clean_expired(self) -> None:
|
||||
"""Background thread to clean expired entries."""
|
||||
while True:
|
||||
time.sleep(60) # Clean every minute
|
||||
with self._lock:
|
||||
expired_keys = [
|
||||
key for key, entry in self._cache.items()
|
||||
if entry.is_expired()
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self._cache[key]
|
||||
self._evictions += 1
|
||||
|
||||
if expired_keys:
|
||||
logger.info(f"Cleaned {len(expired_keys)} expired cache entries")
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get cache statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with cache statistics
|
||||
"""
|
||||
with self._lock:
|
||||
return {
|
||||
'size': len(self._cache),
|
||||
'hits': self._hits,
|
||||
'misses': self._misses,
|
||||
'hit_ratio': self._hits / (self._hits + self._misses) if (self._hits + self._misses) > 0 else 0,
|
||||
'evictions': self._evictions,
|
||||
'max_size': self.max_size
|
||||
}
|
||||
|
||||
def keys(self) -> List[str]:
|
||||
"""Get all cache keys."""
|
||||
with self._lock:
|
||||
return list(self._cache.keys())
|
||||
|
||||
# Global cache instance
|
||||
cache = FastMemoryCache(max_size=2000, default_timeout=300)
|
||||
|
||||
def cached(timeout: Optional[int] = None, unless: Optional[Callable] = None):
|
||||
"""
|
||||
Decorator for caching function results.
|
||||
|
||||
Args:
|
||||
timeout: Cache timeout in seconds
|
||||
unless: Callable that returns True to bypass cache
|
||||
"""
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Bypass cache if unless condition is met
|
||||
if unless and unless():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# Create cache key from function name and arguments
|
||||
key_parts = [func.__module__, func.__name__]
|
||||
key_parts.extend(str(arg) for arg in args)
|
||||
key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
|
||||
key = "|".join(key_parts)
|
||||
|
||||
# Try to get from cache
|
||||
cached_result = cache.get(key)
|
||||
if cached_result is not None:
|
||||
logger.info(f"Cache hit for {func.__name__}")
|
||||
return cached_result
|
||||
|
||||
# Call function and cache result
|
||||
result = func(*args, **kwargs)
|
||||
cache.set(key, result, timeout)
|
||||
logger.info(f"Cache miss for {func.__name__}, caching result")
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def cache_clear() -> None:
|
||||
"""Clear the entire cache."""
|
||||
cache.clear()
|
||||
logger.info("Cache cleared")
|
||||
|
||||
def cache_stats() -> Dict[str, Any]:
|
||||
"""Get cache statistics."""
|
||||
return cache.get_stats()
|
||||
@@ -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()
|
||||
@@ -1,376 +1,385 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import sqlite3
|
||||
import threading
|
||||
import random
|
||||
import tempfile
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
WATCHDOG_AVAILABLE = True
|
||||
except ImportError:
|
||||
WATCHDOG_AVAILABLE = False
|
||||
|
||||
PROBLEMS_DIR = Path(__file__).parent / 'problems'
|
||||
DB_PATH = Path(__file__).parent / '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 = []
|
||||
if not PROBLEMS_DIR.exists():
|
||||
print(f"Problems directory does not exist: {PROBLEMS_DIR}")
|
||||
return problems
|
||||
|
||||
for folder in PROBLEMS_DIR.iterdir():
|
||||
if folder.is_dir():
|
||||
# Dynamically find manifest file (manifest.json or manifets.json)
|
||||
manifest_path = None
|
||||
for candidate in ["manifest.json", "manifets.json"]:
|
||||
candidate_path = folder / candidate
|
||||
if candidate_path.exists():
|
||||
manifest_path = candidate_path
|
||||
break
|
||||
|
||||
desc_path = folder / 'description.md'
|
||||
test_path = folder / 'test.py'
|
||||
|
||||
# Check if required files exist
|
||||
if manifest_path and desc_path.exists() and test_path.exists():
|
||||
try:
|
||||
with open(desc_path, 'r', encoding='utf-8') as f:
|
||||
description = f.read()
|
||||
with open(test_path, 'r', encoding='utf-8') as f:
|
||||
test_code = f.read()
|
||||
|
||||
problems.append({
|
||||
'folder': folder.name,
|
||||
'description': description,
|
||||
'test_code': test_code
|
||||
})
|
||||
print(f"Found problem: {folder.name}")
|
||||
except Exception as e:
|
||||
print(f"Error reading problem files for {folder.name}: {e}")
|
||||
else:
|
||||
missing_files = []
|
||||
if not manifest_path:
|
||||
missing_files.append("manifest.json/manifets.json")
|
||||
if not desc_path.exists():
|
||||
missing_files.append("description.md")
|
||||
if not test_path.exists():
|
||||
missing_files.append("test.py")
|
||||
print(f"Skipping {folder.name}: missing {', '.join(missing_files)}")
|
||||
|
||||
print(f"Total problems found: {len(problems)}")
|
||||
return problems
|
||||
|
||||
def update_db(self, problems, retries=5):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH, timeout=5)
|
||||
c = conn.cursor()
|
||||
c.execute('PRAGMA journal_mode=WAL;')
|
||||
|
||||
# Clear existing problems
|
||||
c.execute('DELETE FROM problems')
|
||||
|
||||
# Insert new problems
|
||||
for p in problems:
|
||||
c.execute('INSERT INTO problems (folder, description, test_code) VALUES (?, ?, ?)',
|
||||
(p['folder'], p['description'], p['test_code']))
|
||||
|
||||
conn.commit()
|
||||
print(f"Updated database with {len(problems)} problems")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
if 'locked' in str(e).lower():
|
||||
wait_time = 0.2 + random.random() * 0.3
|
||||
print(f"Database locked, retrying in {wait_time:.2f}s (attempt {attempt + 1})")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
print(f"Database error: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Unexpected error updating database: {e}")
|
||||
raise
|
||||
|
||||
print('Failed to update problems DB after several retries due to lock.')
|
||||
|
||||
def rescan_and_update(self):
|
||||
print("Scanning for problems...")
|
||||
problems = self.scan()
|
||||
self.update_db(problems)
|
||||
|
||||
def run(self):
|
||||
print("Starting problem scanner...")
|
||||
|
||||
# Initial scan and table creation
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
self.create_table(conn)
|
||||
conn.close()
|
||||
print("Database initialized")
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize database: {e}")
|
||||
return
|
||||
|
||||
# Initial scan
|
||||
self.rescan_and_update()
|
||||
|
||||
if WATCHDOG_AVAILABLE:
|
||||
print("Using watchdog for file monitoring")
|
||||
|
||||
class Handler(FileSystemEventHandler):
|
||||
def __init__(self, scanner):
|
||||
self.scanner = scanner
|
||||
self.last_event_time = 0
|
||||
|
||||
def on_any_event(self, event):
|
||||
# Debounce events to avoid too many rescans
|
||||
now = time.time()
|
||||
if now - self.last_event_time > 1: # Wait at least 1 second between rescans
|
||||
self.last_event_time = now
|
||||
print(f"File system event: {event.event_type} - {event.src_path}")
|
||||
self.scanner.rescan_and_update()
|
||||
|
||||
event_handler = Handler(self)
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(event_handler, str(PROBLEMS_DIR), recursive=True)
|
||||
self.observer.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping problem scanner...")
|
||||
finally:
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
else:
|
||||
print(f"Watchdog not available, using polling every {self.scan_interval}s")
|
||||
# Fallback: poll every scan_interval seconds
|
||||
try:
|
||||
while True:
|
||||
time.sleep(self.scan_interval)
|
||||
self.rescan_and_update()
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping problem scanner...")
|
||||
|
||||
def start_problem_scanner():
|
||||
scanner = ProblemScannerThread()
|
||||
scanner.start()
|
||||
return scanner
|
||||
|
||||
# Flask model loading functions
|
||||
def load_problems_from_json(json_path):
|
||||
"""Load problems from JSON file into Flask database"""
|
||||
if not os.path.exists(json_path):
|
||||
print(f"Problem JSON file not found: {json_path}")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
problems = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error reading JSON file: {e}")
|
||||
return
|
||||
|
||||
# This assumes you have imported the necessary Flask/SQLAlchemy components
|
||||
try:
|
||||
from models import db, Problem
|
||||
|
||||
for p in problems:
|
||||
# Check if problem already exists by title
|
||||
existing = Problem.query.filter_by(title=p['title']).first()
|
||||
|
||||
# Load test code from solution file if provided
|
||||
test_code = ''
|
||||
if 'solution' in p and os.path.exists(p['solution']):
|
||||
try:
|
||||
with open(p['solution'], 'r', encoding='utf-8') as sf:
|
||||
test_code = sf.read()
|
||||
except Exception as e:
|
||||
print(f"Error reading solution file for {p['title']}: {e}")
|
||||
|
||||
if existing:
|
||||
existing.description = p['description']
|
||||
existing.test_code = test_code
|
||||
print(f"Updated problem: {p['title']}")
|
||||
else:
|
||||
new_problem = Problem(title=p['title'], description=p['description'], test_code=test_code)
|
||||
db.session.add(new_problem)
|
||||
print(f"Added new problem: {p['title']}")
|
||||
|
||||
db.session.commit()
|
||||
print("Successfully updated problems from JSON")
|
||||
|
||||
except ImportError:
|
||||
print("Flask models not available - skipping JSON load")
|
||||
except Exception as e:
|
||||
print(f"Error loading problems from JSON: {e}")
|
||||
|
||||
def schedule_problem_reload(app, json_path, interval_hours=10):
|
||||
"""Schedule periodic reloading of problems from JSON"""
|
||||
def reload_loop():
|
||||
while True:
|
||||
try:
|
||||
with app.app_context():
|
||||
load_problems_from_json(json_path)
|
||||
time.sleep(interval_hours * 3600)
|
||||
except Exception as e:
|
||||
print(f"Error in problem reload loop: {e}")
|
||||
time.sleep(60) # Wait 1 minute before retrying
|
||||
|
||||
t = threading.Thread(target=reload_loop, daemon=True)
|
||||
t.start()
|
||||
|
||||
def run_code_against_tests(user_code, test_code, timeout=10):
|
||||
"""
|
||||
Execute user code against test code with proper error handling.
|
||||
|
||||
Args:
|
||||
user_code: The user's solution code
|
||||
test_code: The test code to validate the solution
|
||||
timeout: Maximum execution time in seconds
|
||||
|
||||
Returns:
|
||||
dict: Result with passed, output, runtime, and error fields
|
||||
"""
|
||||
if not user_code or not user_code.strip():
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': 'No code provided'
|
||||
}
|
||||
|
||||
if not test_code or not test_code.strip():
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': 'No test code available'
|
||||
}
|
||||
|
||||
start_time = time.perf_counter()
|
||||
output = ''
|
||||
error = None
|
||||
passed = False
|
||||
temp_file = None
|
||||
|
||||
try:
|
||||
# Check if unittest is used in test_code
|
||||
if 'unittest' in test_code:
|
||||
# Create temporary file with user code + test code
|
||||
with tempfile.NamedTemporaryFile('w+', suffix='.py', delete=False, encoding='utf-8') as f:
|
||||
# Combine user code and test code
|
||||
combined_code = f"{user_code}\n\n{test_code}"
|
||||
f.write(combined_code)
|
||||
f.flush()
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Run the file as a subprocess with timeout
|
||||
proc = subprocess.run(
|
||||
[sys.executable, temp_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
output = proc.stdout
|
||||
if proc.stderr:
|
||||
output += f"\nSTDERR:\n{proc.stderr}"
|
||||
|
||||
passed = proc.returncode == 0
|
||||
if not passed:
|
||||
error = f"Tests failed. Return code: {proc.returncode}\n{output}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
passed = False
|
||||
error = f"Code execution timed out after {timeout} seconds"
|
||||
output = "Execution timed out"
|
||||
|
||||
else:
|
||||
# Direct execution approach for simple assert-based tests
|
||||
local_ns = {}
|
||||
|
||||
# Capture stdout
|
||||
old_stdout = sys.stdout
|
||||
captured_output = io.StringIO()
|
||||
sys.stdout = captured_output
|
||||
|
||||
try:
|
||||
# Execute user code first
|
||||
exec(user_code, {}, local_ns)
|
||||
|
||||
# Execute test code in the same namespace
|
||||
exec(test_code, local_ns, local_ns)
|
||||
|
||||
# If we get here without exceptions, tests passed
|
||||
passed = True
|
||||
|
||||
except AssertionError as e:
|
||||
passed = False
|
||||
error = f"Assertion failed: {str(e)}"
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Runtime error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
output = captured_output.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Execution error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not delete temp file {temp_file}: {e}")
|
||||
|
||||
runtime = time.perf_counter() - start_time
|
||||
|
||||
result = {
|
||||
'passed': passed,
|
||||
'output': output.strip() if output else '',
|
||||
'runtime': runtime,
|
||||
'error': error if not passed else None
|
||||
}
|
||||
|
||||
print(f"Test execution result: passed={passed}, runtime={runtime:.3f}s")
|
||||
if error:
|
||||
print(f"Error: {error}")
|
||||
|
||||
return result
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import sqlite3
|
||||
import threading
|
||||
import random
|
||||
import tempfile
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
WATCHDOG_AVAILABLE = True
|
||||
except ImportError:
|
||||
WATCHDOG_AVAILABLE = False
|
||||
|
||||
PROBLEMS_DIR = Path(__file__).parent / 'problems'
|
||||
DB_PATH = Path(__file__).parent / 'database/problems.sqlite3'
|
||||
|
||||
class ProblemScannerThread(threading.Thread):
|
||||
def __init__(self, scan_interval=2):
|
||||
super().__init__(daemon=True)
|
||||
self.scan_interval = scan_interval
|
||||
self.last_state = {}
|
||||
self.observer = None
|
||||
|
||||
def create_table(self, conn):
|
||||
c = conn.cursor()
|
||||
c.execute('PRAGMA journal_mode=WAL;')
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS problems (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
folder TEXT,
|
||||
description TEXT,
|
||||
difficulty TEXT,
|
||||
test_code TEXT
|
||||
)''')
|
||||
conn.commit()
|
||||
|
||||
def scan(self):
|
||||
problems = []
|
||||
if not PROBLEMS_DIR.exists():
|
||||
print(f"Problems directory does not exist: {PROBLEMS_DIR}")
|
||||
return problems
|
||||
|
||||
for folder in PROBLEMS_DIR.iterdir():
|
||||
if folder.is_dir():
|
||||
# Dynamically find manifest file (manifest.json or manifests.json)
|
||||
manifest_path = None
|
||||
for candidate in ["manifest.json", "manifests.json"]:
|
||||
candidate_path = folder / candidate
|
||||
if candidate_path.exists():
|
||||
manifest_path = candidate_path
|
||||
break
|
||||
|
||||
desc_path = folder / 'description.md'
|
||||
test_path = folder / 'test.py'
|
||||
|
||||
# Check if required files exist
|
||||
if manifest_path and desc_path.exists() and test_path.exists():
|
||||
try:
|
||||
with open(desc_path, 'r', encoding='utf-8') as f:
|
||||
description = f.read()
|
||||
with open(test_path, 'r', encoding='utf-8') as f:
|
||||
test_code = f.read()
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest = json.load(f)
|
||||
|
||||
difficulty = manifest.get('difficulty', 'unknown')
|
||||
|
||||
problems.append({
|
||||
'folder': folder.name,
|
||||
'description': description,
|
||||
'test_code': test_code,
|
||||
'difficulty': difficulty
|
||||
})
|
||||
print(f"[ INFO ]: Found problem: {folder.name} ; Difficulty: {difficulty}")
|
||||
except Exception as e:
|
||||
print(f"[ ERROR ]: Error reading problem files for {folder.name}: {e}")
|
||||
else:
|
||||
missing_files = []
|
||||
if not manifest_path:
|
||||
missing_files.append("manifest.json/manifets.json")
|
||||
if not desc_path.exists():
|
||||
missing_files.append("description.md")
|
||||
if not test_path.exists():
|
||||
missing_files.append("test.py")
|
||||
print(f"[ SKIP ]: Skipping {folder.name}: missing {', '.join(missing_files)}")
|
||||
|
||||
print(f"[ INFO ]: Total problems found: {len(problems)}")
|
||||
return problems
|
||||
|
||||
def update_db(self, problems, retries=5):
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH, timeout=5)
|
||||
c = conn.cursor()
|
||||
c.execute('PRAGMA journal_mode=WAL;')
|
||||
|
||||
# Clear existing problems
|
||||
c.execute('DELETE FROM problems')
|
||||
|
||||
# Insert new problems
|
||||
for p in problems:
|
||||
c.execute('''INSERT INTO problems
|
||||
(folder, description, difficulty, test_code)
|
||||
VALUES (?, ?, ?, ?)''',
|
||||
(p['folder'], p['description'], p['difficulty'], p['test_code']))
|
||||
|
||||
conn.commit()
|
||||
print(f"[ INFO ]: Updated database with {len(problems)} problems")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
if 'locked' in str(e).lower():
|
||||
wait_time = 0.2 + random.random() * 0.3
|
||||
print(f"[ WARNING ]: Database locked, retrying in {wait_time:.2f}s (attempt {attempt + 1})")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
print(f"[ ERROR ]: Database error: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[ ERROR ]: Unexpected error updating database: {e}")
|
||||
raise
|
||||
|
||||
print('[ FATAL ERROR ]: Failed to update problems DB after several retries due to lock.')
|
||||
|
||||
def rescan_and_update(self):
|
||||
print("[ INFO ]: Scanning for problems...")
|
||||
problems = self.scan()
|
||||
self.update_db(problems)
|
||||
|
||||
def run(self):
|
||||
print("[ INFO ]: Starting problem scanner...")
|
||||
|
||||
# Initial scan and table creation
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
self.create_table(conn)
|
||||
conn.close()
|
||||
print("[ INFO ]: Database initialized")
|
||||
except Exception as e:
|
||||
print(f"[ FATAL ERROR ]: Failed to initialize database: {e}")
|
||||
return
|
||||
|
||||
# Initial scan
|
||||
self.rescan_and_update()
|
||||
|
||||
if WATCHDOG_AVAILABLE:
|
||||
print("[ INFO ]: Using watchdog for file monitoring")
|
||||
|
||||
class Handler(FileSystemEventHandler):
|
||||
def __init__(self, scanner):
|
||||
self.scanner = scanner
|
||||
self.last_event_time = 0
|
||||
|
||||
def on_any_event(self, event):
|
||||
# Debounce events to avoid too many rescans
|
||||
now = time.time()
|
||||
if now - self.last_event_time > 1: # Wait at least 1 second between rescans
|
||||
self.last_event_time = now
|
||||
print(f"[ FSINFO ]: File system event: {event.event_type} - {event.src_path}")
|
||||
self.scanner.rescan_and_update()
|
||||
|
||||
event_handler = Handler(self)
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(event_handler, str(PROBLEMS_DIR), recursive=True)
|
||||
self.observer.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("[ KBINT_INFO ]: Stopping problem scanner...")
|
||||
finally:
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
else:
|
||||
print(f"[ WARNING ]: Watchdog not available, using polling every {self.scan_interval}s")
|
||||
# Fallback: poll every scan_interval seconds
|
||||
try:
|
||||
while True:
|
||||
time.sleep(self.scan_interval)
|
||||
self.rescan_and_update()
|
||||
except KeyboardInterrupt:
|
||||
print("[ KBINT_INFO ]: Stopping problem scanner...")
|
||||
|
||||
def start_problem_scanner():
|
||||
scanner = ProblemScannerThread()
|
||||
scanner.start()
|
||||
return scanner
|
||||
|
||||
# Flask model loading functions
|
||||
def load_problems_from_json(json_path):
|
||||
"""Load problems from JSON file into Flask database"""
|
||||
if not os.path.exists(json_path):
|
||||
print(f"[ DEPRECATED_INFO ]: Problem JSON file not found: {json_path}")
|
||||
print("[ SUGGESTION ]: If you dont have this do not worry. Use mainfest.json!")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
problems = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"[ ERROR ]: Error reading JSON file: {e}")
|
||||
return
|
||||
|
||||
# This assumes you have imported the necessary Flask/SQLAlchemy components
|
||||
try:
|
||||
from models import db, Problem
|
||||
|
||||
for p in problems:
|
||||
# Check if problem already exists by title
|
||||
existing = Problem.query.filter_by(title=p['title']).first()
|
||||
|
||||
# Load test code from solution file if provided
|
||||
test_code = ''
|
||||
if 'solution' in p and os.path.exists(p['solution']):
|
||||
try:
|
||||
with open(p['solution'], 'r', encoding='utf-8') as sf:
|
||||
test_code = sf.read()
|
||||
except Exception as e:
|
||||
print(f"[ FATAL ERROR ]: Error reading solution file for {p['title']}: {e}")
|
||||
|
||||
if existing:
|
||||
existing.description = p['description']
|
||||
existing.test_code = test_code
|
||||
print(f"[ INFO ]: Updated problem: {p['title']}")
|
||||
else:
|
||||
new_problem = Problem(title=p['title'], description=p['description'], test_code=test_code)
|
||||
db.session.add(new_problem)
|
||||
print(f"[ SUCCESS ]: Added new problem: {p['title']}")
|
||||
|
||||
db.session.commit()
|
||||
print("[ SUCCESS ]: Successfully updated problems from JSON")
|
||||
|
||||
except ImportError:
|
||||
print("[ FATAL IMPORT ERROR ]: Flask models not available - skipping JSON load @execptImportError")
|
||||
except Exception as e:
|
||||
print(f"[ ERROR ]: Error loading problems from JSON: {e}")
|
||||
|
||||
def schedule_problem_reload(app, json_path, interval_hours=10):
|
||||
"""Schedule periodic reloading of problems from JSON"""
|
||||
def reload_loop():
|
||||
while True:
|
||||
try:
|
||||
with app.app_context():
|
||||
load_problems_from_json(json_path)
|
||||
time.sleep(interval_hours * 3600)
|
||||
except Exception as e:
|
||||
print(f"[ FATAL ERROR ]: Error in problem reload loop: {e}")
|
||||
time.sleep(60) # Wait 1 minute before retrying
|
||||
|
||||
t = threading.Thread(target=reload_loop, daemon=True)
|
||||
t.start()
|
||||
|
||||
def run_code_against_tests(user_code, test_code, timeout=10):
|
||||
"""
|
||||
Execute user code against test code with proper error handling.
|
||||
|
||||
Args:
|
||||
user_code: The user's solution code
|
||||
test_code: The test code to validate the solution
|
||||
timeout: Maximum execution time in seconds
|
||||
|
||||
Returns:
|
||||
dict: Result with passed, output, runtime, and error fields
|
||||
"""
|
||||
if not user_code or not user_code.strip():
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': 'No code provided'
|
||||
}
|
||||
|
||||
if not test_code or not test_code.strip():
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': 'No test code available'
|
||||
}
|
||||
|
||||
start_time = time.perf_counter()
|
||||
output = ''
|
||||
error = None
|
||||
passed = False
|
||||
temp_file = None
|
||||
|
||||
try:
|
||||
# Check if unittest is used in test_code
|
||||
if 'unittest' in test_code:
|
||||
# Create temporary file with user code + test code
|
||||
with tempfile.NamedTemporaryFile('w+', suffix='.py', delete=False, encoding='utf-8') as f:
|
||||
# Combine user code and test code
|
||||
combined_code = f"{user_code}\n\n{test_code}"
|
||||
f.write(combined_code)
|
||||
f.flush()
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Run the file as a subprocess with timeout
|
||||
proc = subprocess.run(
|
||||
[sys.executable, temp_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
output = proc.stdout
|
||||
if proc.stderr:
|
||||
output += f"\nSTDERR:\n{proc.stderr}"
|
||||
|
||||
passed = proc.returncode == 0
|
||||
if not passed:
|
||||
error = f"Tests failed. Return code: {proc.returncode}\n{output}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
passed = False
|
||||
error = f"Code execution timed out after {timeout} seconds"
|
||||
output = "Execution timed out"
|
||||
|
||||
else:
|
||||
# Direct execution approach for simple assert-based tests
|
||||
local_ns = {}
|
||||
|
||||
# Capture stdout
|
||||
old_stdout = sys.stdout
|
||||
captured_output = io.StringIO()
|
||||
sys.stdout = captured_output
|
||||
|
||||
try:
|
||||
# Execute user code first
|
||||
exec(user_code, {}, local_ns)
|
||||
|
||||
# Execute test code in the same namespace
|
||||
exec(test_code, local_ns, local_ns)
|
||||
|
||||
# If we get here without exceptions, tests passed
|
||||
passed = True
|
||||
|
||||
except AssertionError as e:
|
||||
passed = False
|
||||
error = f"Assertion failed: {str(e)}"
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Runtime error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
output = captured_output.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Execution error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except Exception as e:
|
||||
print(f"[ FATAL WARNING ]: Could not delete temp file {temp_file}: {e}")
|
||||
|
||||
runtime = time.perf_counter() - start_time
|
||||
|
||||
result = {
|
||||
'passed': passed,
|
||||
'output': output.strip() if output else '',
|
||||
'runtime': runtime,
|
||||
'error': error if not passed else None
|
||||
}
|
||||
|
||||
print(f"[ TEST RESULT ]: passed={passed}, runtime={runtime:.3f}s")
|
||||
if error:
|
||||
print(f"Error: {error}")
|
||||
|
||||
return result
|
||||
50
src/problems/Hashmaps/description.md
Normal file
50
src/problems/Hashmaps/description.md
Normal file
@@ -0,0 +1,50 @@
|
||||
## 🏷️ Problem: Lost & Found Office
|
||||
|
||||
You are designing a system for a **Lost-and-Found office**.
|
||||
|
||||
* People can **report lost items**, where each item is mapped to the owner’s name.
|
||||
* People can later **claim their item**.
|
||||
* If the item is not found, return `"No item found!"`.
|
||||
|
||||
---
|
||||
|
||||
### Function Signature
|
||||
|
||||
```python
|
||||
class LostAndFound:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def add_item(self, owner: str, item: str) -> None:
|
||||
"""
|
||||
Stores the item with the owner's name.
|
||||
"""
|
||||
|
||||
def claim_item(self, owner: str) -> str:
|
||||
"""
|
||||
Returns the owner's item if it exists, otherwise
|
||||
returns 'No item found!'.
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
office = LostAndFound()
|
||||
office.add_item("Alice", "Umbrella")
|
||||
office.add_item("Bob", "Backpack")
|
||||
|
||||
print(office.claim_item("Alice")) # Output: "Umbrella"
|
||||
print(office.claim_item("Alice")) # Output: "No item found!"
|
||||
print(office.claim_item("Charlie")) # Output: "No item found!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Constraints
|
||||
|
||||
* `1 <= len(owner), len(item) <= 100`
|
||||
* You may assume only **strings** are used for owner and item.
|
||||
* An owner can only have **one item** at a time.
|
||||
7
src/problems/Hashmaps/manifest.json
Normal file
7
src/problems/Hashmaps/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Hashmaps",
|
||||
"description": "DSA - Hashmap. With Lost & Found",
|
||||
"description_md": "problems/Hashmaps/description.md",
|
||||
"test_code": "problems/Hashmaps/test.py",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
54
src/problems/Hashmaps/test.py
Normal file
54
src/problems/Hashmaps/test.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#class LostAndFound:
|
||||
# def __init__(self):
|
||||
# self.items = {} # hashmap: owner -> item
|
||||
#
|
||||
# def add_item(self, owner: str, item: str) -> None:
|
||||
# self.items[owner] = item
|
||||
#
|
||||
# def claim_item(self, owner: str) -> str:
|
||||
# return self.items.pop(owner, "No item found!")
|
||||
|
||||
import unittest
|
||||
|
||||
class TestLostAndFound(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
office = LostAndFound()
|
||||
office.add_item("Alice", "Umbrella")
|
||||
office.add_item("Bob", "Backpack")
|
||||
|
||||
test_cases = [
|
||||
("Alice", "Umbrella", "First claim for Alice"),
|
||||
("Alice", "No item found!", "Alice claims again (should fail)"),
|
||||
("Charlie", "No item found!", "Charlie never added an item"),
|
||||
]
|
||||
|
||||
print("\nTEST: Basic LostAndFound Behavior")
|
||||
for name, expected, description in test_cases:
|
||||
try:
|
||||
actual = office.claim_item(name)
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | {description} | Input: {name} -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | {description} | Input: {name} -> Exception: {e}")
|
||||
raise
|
||||
|
||||
def test_overwrite_item(self):
|
||||
office = LostAndFound()
|
||||
office.add_item("Bob", "Hat")
|
||||
office.add_item("Bob", "Shoes") # overwrite
|
||||
|
||||
print("\nTEST: Overwriting Items")
|
||||
try:
|
||||
actual = office.claim_item("Bob")
|
||||
expected = "Shoes"
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Overwritten item claim | Input: Bob -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Overwritten item claim | Input: Bob -> Exception: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
60
src/problems/Palindrome/description.md
Normal file
60
src/problems/Palindrome/description.md
Normal file
@@ -0,0 +1,60 @@
|
||||
## Problem: Check if a String is a Palindrome
|
||||
|
||||
Given a string `s`, determine whether it reads the same forward and backward.
|
||||
Return `True` if it is a palindrome, otherwise return `False`.
|
||||
|
||||
A **palindrome** is a sequence of characters that is identical when reversed.
|
||||
Comparison is **case-sensitive** and should consider all characters, including spaces and punctuation.
|
||||
|
||||
---
|
||||
|
||||
### Example 1
|
||||
|
||||
**Input:**
|
||||
|
||||
```
|
||||
s = "racecar"
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
True
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
Reversing `"racecar"` results in `"racecar"`, which is the same as the original string.
|
||||
|
||||
---
|
||||
|
||||
### Example 2
|
||||
|
||||
**Input:**
|
||||
|
||||
```
|
||||
s = "hello"
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
False
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
Reversing `"hello"` results in `"olleh"`, which is different from the original string.
|
||||
|
||||
---
|
||||
|
||||
### Constraints
|
||||
|
||||
* `0 <= len(s) <= 10^5`
|
||||
* `s` may contain letters, digits, symbols, and spaces.
|
||||
|
||||
---
|
||||
|
||||
### Function Signature (Python)
|
||||
|
||||
```python
|
||||
def palindrome(s: str) -> bool:
|
||||
```
|
||||
7
src/problems/Palindrome/manifest.json
Normal file
7
src/problems/Palindrome/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Palindrome",
|
||||
"description": "Find out wether or not a String is a Palindrome",
|
||||
"description_md": "problems/Palindrome/description.md",
|
||||
"test_code": "problems/Palindrome/test.py",
|
||||
"difficulty": "medium"
|
||||
}
|
||||
32
src/problems/Palindrome/test.py
Normal file
32
src/problems/Palindrome/test.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import unittest
|
||||
|
||||
#<!-- User expected Function -->
|
||||
## def palindrome(s:str) -> bool:
|
||||
## return s == s[::-1]
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_palindrome(self):
|
||||
test_cases = [
|
||||
("racecar", True), # Simple palindrome
|
||||
("hello", False), # Not a palindrome
|
||||
("", True), # Empty string
|
||||
("a", True), # Single character
|
||||
("madam", True), # Palindrome word
|
||||
("Madam", False), # Case-sensitive check
|
||||
("12321", True), # Numeric string palindrome
|
||||
("123456", False), # Numeric string non-palindrome
|
||||
]
|
||||
print("\nFUNCTION OUTPUT TEST RESULTS")
|
||||
|
||||
for input_val, expected in test_cases:
|
||||
try:
|
||||
actual = palindrome(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: '{input_val}' -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: '{input_val}' -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
90
src/problems/PrimeNumber/description.md
Normal file
90
src/problems/PrimeNumber/description.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Prime Number Function Checker
|
||||
|
||||
You are asked to **write a function** that checks if a number is a **prime number**.
|
||||
|
||||
### What is a Prime Number?
|
||||
|
||||
* A **prime number** is a whole number greater than `1`.
|
||||
* It has only **two divisors**: `1` and the number itself.
|
||||
* Example:
|
||||
|
||||
* `7` → Prime (divisible only by `1` and `7`)
|
||||
* `8` → Not Prime (divisible by `1, 2, 4, 8`)
|
||||
|
||||
Numbers less than or equal to `1` are **not prime**.
|
||||
|
||||
📖 More info: [Wikipedia](https://en.wikipedia.org/wiki/Prime_number)
|
||||
|
||||
---
|
||||
|
||||
### Function Signature
|
||||
|
||||
```python
|
||||
def check_prime(number: int) -> bool:
|
||||
```
|
||||
|
||||
* **Input**:
|
||||
|
||||
* `number` → an integer
|
||||
|
||||
* **Output**:
|
||||
|
||||
* `True` → if the number is prime
|
||||
* `False` → if the number is not prime
|
||||
|
||||
---
|
||||
|
||||
### Example 1
|
||||
|
||||
**Input:**
|
||||
|
||||
```python
|
||||
check_prime(2)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 2
|
||||
|
||||
**Input:**
|
||||
|
||||
```python
|
||||
check_prime(4)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 3
|
||||
|
||||
**Input:**
|
||||
|
||||
```python
|
||||
check_prime(13)
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**_Dont worry you do NOT need to write these Function Calls into your solution. QPP checks automatically_**
|
||||
|
||||
### Hint
|
||||
|
||||
Try using the **modulo operator `%`** to check if one number divides evenly into another.
|
||||
If any number between `2` and `n-1` divides your number evenly, then it’s **not prime**.
|
||||
7
src/problems/PrimeNumber/manifest.json
Normal file
7
src/problems/PrimeNumber/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Prime Number Checker",
|
||||
"description": "Determine if a given number is a prime number",
|
||||
"description_md": "problems/PrimeNumber/description.md",
|
||||
"test_code": "problems/PrimeNumber/test.py",
|
||||
"difficulty": "medium"
|
||||
}
|
||||
33
src/problems/PrimeNumber/test.py
Normal file
33
src/problems/PrimeNumber/test.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import unittest
|
||||
|
||||
|
||||
# <!-- Function to check -->
|
||||
# def check_prime(number : int) -> bool:
|
||||
# for i in range(2, int(number)):
|
||||
# if int(number) % i == 0:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
class TestPrimeNumber(unittest.TestCase):
|
||||
def test_prime_function(self):
|
||||
test_cases = [
|
||||
(2,True),
|
||||
(3,True),
|
||||
(4,False),
|
||||
(6,False),
|
||||
(1,False)
|
||||
]
|
||||
print("\nFUNCTION OUTPUT TEST RESULTS")
|
||||
|
||||
for input_val, expected in test_cases:
|
||||
try:
|
||||
actual = check_prime(input_val)
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: '{input_val}' -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: '{input_val}' -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
44
src/problems/ReversedList/description.md
Normal file
44
src/problems/ReversedList/description.md
Normal file
@@ -0,0 +1,44 @@
|
||||
## Reverse a List
|
||||
|
||||
Write a function called `reverse_list` that takes a list as input and returns the list in reverse order.
|
||||
You are **allowed** to just use Python’s built-in `.reverse()` method or slicing (`[::-1]`), try to reverse it manually for practice.
|
||||
|
||||
### Function Signature:
|
||||
|
||||
```python
|
||||
def reverse_list(lst):
|
||||
# your code here
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
* The function should return a new list with the elements in reversed order.
|
||||
* The input list can contain:
|
||||
|
||||
* Numbers
|
||||
* Strings
|
||||
* Booleans
|
||||
* A mix of different types
|
||||
* Your function will be tested with:
|
||||
|
||||
* A small list (e.g., `[1, 2, 3]` → `[3, 2, 1]`)
|
||||
* A longer list (e.g., `[1, 2, 3, 4]` → `[4, 3, 2, 1]`)
|
||||
* An empty list (should return an empty list)
|
||||
* A single-element list (should return the same list)
|
||||
* A mixed-type list (e.g., `[1, 'a', True]` → `[True, 'a', 1]`)
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
reverse_list([1, 2, 3])
|
||||
# Output: [3, 2, 1]
|
||||
|
||||
reverse_list([])
|
||||
# Output: []
|
||||
|
||||
reverse_list([5])
|
||||
# Output: [5]
|
||||
|
||||
reverse_list([1, 'a', True])
|
||||
# Output: [True, 'a', 1]
|
||||
```
|
||||
7
src/problems/ReversedList/manifest.json
Normal file
7
src/problems/ReversedList/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Reversed List",
|
||||
"description": "Given a list, return a new list with the elements in reverse order.",
|
||||
"description_md": "problems/reversedlist/description.md",
|
||||
"difficulty": "easy",
|
||||
"test_code": "problems/reversedlist/test.py"
|
||||
}
|
||||
27
src/problems/ReversedList/test.py
Normal file
27
src/problems/ReversedList/test.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
|
||||
#def reverse_list(lst : list) -> list:
|
||||
#return lst[::-1]
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
test_cases = [
|
||||
([1, 2, 3], [3, 2, 1]), # Simple case
|
||||
([1, 2, 3, 4], [4, 3, 2, 1]), # Longer list
|
||||
([], []), # Empty list
|
||||
([5], [5]), # Single element list
|
||||
([1, 'a', True], [True, 'a', 1]) # Mixed types
|
||||
]
|
||||
print("\n FUNCTION OUTPUT TEST RESULTS")
|
||||
|
||||
for input_val , expected in test_cases:
|
||||
try:
|
||||
actual = reverse_list(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: {input_val} -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: {input_val} -> Exception: {e}")
|
||||
raise
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
40
src/problems/fibonacisequence/description.md
Normal file
40
src/problems/fibonacisequence/description.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Fibonacci Number
|
||||
|
||||
Write a function called `fibonacci` that takes a non-negative integer `n` as input and returns the **n-th Fibonacci number**.
|
||||
|
||||
The Fibonacci sequence is defined as:
|
||||
|
||||
* `F(0) = 0`
|
||||
* `F(1) = 1`
|
||||
* `F(n) = F(n-1) + F(n-2)` for `n > 1`
|
||||
|
||||
### Function Signature:
|
||||
|
||||
```python
|
||||
def fibonacci(n):
|
||||
# return your solution
|
||||
```
|
||||
|
||||
#### Requirements
|
||||
|
||||
* The function should return the `n`-th number in the Fibonacci sequence.
|
||||
* If `n` is less than `0`, print `"Incorrect input"`.
|
||||
* Your function will be tested with:
|
||||
|
||||
* Base cases (`n = 0` and `n = 1`)
|
||||
* Small values of `n`
|
||||
* Larger values of `n` (e.g., 9)
|
||||
* Multiple test cases in sequence
|
||||
|
||||
#### Example:
|
||||
|
||||
```python
|
||||
fibonacci(0) # returns 0
|
||||
fibonacci(1) # returns 1
|
||||
fibonacci(2) # returns 1
|
||||
fibonacci(3) # returns 2
|
||||
fibonacci(5) # returns 5
|
||||
fibonacci(9) # returns 34
|
||||
```
|
||||
|
||||
You can copy this into your problem’s solution description.
|
||||
7
src/problems/fibonacisequence/manifest.json
Normal file
7
src/problems/fibonacisequence/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Fibonacci Sequence",
|
||||
"description": "Calculate the n-th Fibonacci number using a function. The Fibonacci sequence is defined as follows: F(0) = 0, F(1) = 1, and F(n) = F(n-1) + F(n-2) for n > 1.",
|
||||
"description_md": "problems/fibonacisequence/description.md",
|
||||
"difficulty": "medium",
|
||||
"test_code": "problems/fibonacisequence/test.py"
|
||||
}
|
||||
52
src/problems/fibonacisequence/test.py
Normal file
52
src/problems/fibonacisequence/test.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import unittest
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_simple(self):
|
||||
test_cases = [
|
||||
(0, 0), # Base case: n = 0
|
||||
(1, 1), # Base case: n = 1
|
||||
(2, 1), # Fibonacci(2) = 1
|
||||
(3, 2), # Fibonacci(3) = 2
|
||||
(5, 5), # Fibonacci(5) = 5
|
||||
(9, 34), # Fibonacci(9) = 34
|
||||
]
|
||||
|
||||
print("\n=== Function Output Test Results ===")
|
||||
for input_val, expected in test_cases:
|
||||
try:
|
||||
actual = fibonacci(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: {input_val} -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: {input_val} -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
"""
|
||||
def fibonacci(n):
|
||||
a = 0
|
||||
b = 1
|
||||
|
||||
# Check if n is less than 0
|
||||
if n < 0:
|
||||
print("Incorrect input")
|
||||
|
||||
# Check if n is equal to 0
|
||||
elif n == 0:
|
||||
return 0
|
||||
|
||||
# Check if n is equal to 1
|
||||
elif n == 1:
|
||||
return b
|
||||
else:
|
||||
for i in range(1, n):
|
||||
c = a + b
|
||||
a = b
|
||||
b = c
|
||||
return b
|
||||
|
||||
print(fibonacci(9))
|
||||
"""
|
||||
58
src/problems/regex-phone/description.md
Normal file
58
src/problems/regex-phone/description.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Phone Number Regular Expression Validation
|
||||
|
||||
You are asked to write a function that checks if a given string is a valid phone number.
|
||||
|
||||
A valid phone number must follow this format:
|
||||
|
||||
```python
|
||||
123-456-7890
|
||||
```
|
||||
|
||||
* It contains **3 digits**, followed by a **dash (-)**
|
||||
* Then another **3 digits**, followed by a **dash (-)**
|
||||
* Then exactly **4 digits**
|
||||
|
||||
If the string matches this exact format, return **True**. Otherwise, return **False**.
|
||||
|
||||
---
|
||||
|
||||
### Example 1
|
||||
|
||||
```python
|
||||
Input: "123-456-7890"
|
||||
Output: True
|
||||
```
|
||||
|
||||
### Example 2
|
||||
|
||||
```python
|
||||
Input: "1234567890"
|
||||
Output: False
|
||||
```
|
||||
|
||||
### Example 3
|
||||
|
||||
```python
|
||||
Input: "abc-def-ghij"
|
||||
Output: False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Function Signature
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
def is_valid_phone_number(phone_number: str) -> bool:
|
||||
return bool("Your Solution Here!")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Hint 🔑
|
||||
|
||||
* Use the **`re`** (regular expression) library.
|
||||
* `\d` means “a digit” in regex.
|
||||
* You will need exactly **3 digits**, then a dash, then **3 digits**, another dash, then **4 digits**.
|
||||
* Anchors `^` (start of string) and `$` (end of string) can help ensure the whole string matches.
|
||||
7
src/problems/regex-phone/manifest.json
Normal file
7
src/problems/regex-phone/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Regex Phonenumber",
|
||||
"description": "A regex problem to match phone numbers in various formats.",
|
||||
"description_md": "problems/regex-phone/description.md",
|
||||
"difficulty": "hard",
|
||||
"test_code": "problems/regex-phone/test.py"
|
||||
}
|
||||
33
src/problems/regex-phone/test.py
Normal file
33
src/problems/regex-phone/test.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import re
|
||||
import unittest
|
||||
|
||||
## def is_valid_phone_number(phone_number : str):
|
||||
## return bool(re.search(r"^(\d{3}-){2}\d{4}$", phone_number))
|
||||
|
||||
import unittest
|
||||
|
||||
class TestPhoneNumberRegex(unittest.TestCase):
|
||||
def test_if_valid(self):
|
||||
test_cases = [
|
||||
("123-456-7890", True), # Valid format
|
||||
("111-222-3333", True), # Another valid format
|
||||
("abc-def-ghij", False), # Letters instead of digits
|
||||
("1234567890", False), # Missing dashes
|
||||
("123-45-67890", False), # Wrong grouping
|
||||
("12-3456-7890", False), # Wrong grouping again
|
||||
("", False), # Empty string
|
||||
]
|
||||
print("\nPHONE NUMBER VALIDATION TEST RESULTS")
|
||||
|
||||
for phone, expected in test_cases:
|
||||
try:
|
||||
actual = is_valid_phone_number(phone) # pyright: ignore[reportUndefinedVariable]
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: '{phone}' -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: '{phone}' -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -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"
|
||||
}
|
||||
14
src/problems/sortlist/description.md
Normal file
14
src/problems/sortlist/description.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## Sorting a List
|
||||
|
||||
In this example you are given a Task: **Sort a List of _say_ Apples**.
|
||||
|
||||
## Function Signature:
|
||||
|
||||
```python
|
||||
def sortlist(lst: list) -> list:
|
||||
return # Your solution
|
||||
```
|
||||
|
||||
Using the Type Inferrence gives you a Idea of what to return. You may freely choose to type or not to. The Python Interpreter does not care about Type Inferrence
|
||||
|
||||
Sorting manually may be tedious. Look at the [PyDocs](https://docs.python.org/3/howto/sorting.html#sorting-basics)
|
||||
@@ -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"
|
||||
}
|
||||
26
src/problems/sortlist/test.py
Normal file
26
src/problems/sortlist/test.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import unittest
|
||||
|
||||
#def sortlist(lst = [4,3,2,1]) -> list:
|
||||
#return sorted(lst)
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_sort(self):
|
||||
test_cases=[
|
||||
([3,2,1],[1,2,3]),
|
||||
([4,3,2,1],[1,2,3,4])
|
||||
]
|
||||
|
||||
print("\n Function Output Test Results: ")
|
||||
for input_val, expected in test_cases:
|
||||
try:
|
||||
actual = sortlist(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)
|
||||
BIN
src/static/favicon.ico
Normal file
BIN
src/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
326
src/static/index.css
vendored
Normal file
326
src/static/index.css
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
:root {
|
||||
--bg: #f6f8fb;
|
||||
--card: #fff;
|
||||
--text: #0f172a;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb;
|
||||
--border: #e5e7eb;
|
||||
--hover: #f3f4f6;
|
||||
--shadow: 0 4px 12px rgba(16, 24, 40, 0.06);
|
||||
--radius: 8px;
|
||||
--mono: "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
/* Dark mode variables */
|
||||
html.dark {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--border: #334155;
|
||||
--hover: #334155;
|
||||
--shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
.wrap {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
header h1 {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
color: var(--text);
|
||||
}
|
||||
header p {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.dark-mode-toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.dark-mode-toggle:hover {
|
||||
background: var(--hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
html.dark .dark-mode-icon::before {
|
||||
content: "☀︎️";
|
||||
}
|
||||
html:not(.dark) .dark-mode-icon::before {
|
||||
content: "⏾";
|
||||
}
|
||||
.dark-mode-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
.dark-mode-icon::before {
|
||||
font-size: 1em;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.content.single-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 12px;
|
||||
}
|
||||
/* Search/filter controls */
|
||||
.search-controls {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.filter-select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
/* Problems list */
|
||||
.problems-list .problem-item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.problem-item:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
.problem-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.problem-item a {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.problem-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* Difficulty badge */
|
||||
.difficulty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25em 0.6em;
|
||||
border-radius: 10px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.difficulty[data-difficulty="easy"] {
|
||||
background-color: #4caf50; /* Green */
|
||||
}
|
||||
.difficulty[data-difficulty="medium"] {
|
||||
background-color: #ffc107; /* Amber */
|
||||
color: #333;
|
||||
}
|
||||
.difficulty[data-difficulty="hard"] {
|
||||
background-color: #f44336; /* Red */
|
||||
}
|
||||
/* Leaderboard */
|
||||
.leaderboard-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.leaderboard-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.leaderboard-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.leaderboard-table th,
|
||||
.leaderboard-table td {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
.leaderboard-table th {
|
||||
background: var(--hover);
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.leaderboard-table tr:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
/* Sort indicators */
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.sortable::after {
|
||||
content: "↕";
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.8em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.sort-asc::after {
|
||||
content: "↑";
|
||||
opacity: 1;
|
||||
}
|
||||
.sort-desc::after {
|
||||
content: "↓";
|
||||
opacity: 1;
|
||||
}
|
||||
/* Toggle button */
|
||||
.btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
font-size: 0.85rem;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
.btn.active {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.leaderboard-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Leaderboard horizontal collapse */
|
||||
#leaderboardSection {
|
||||
transition:
|
||||
max-width 0.35s ease,
|
||||
opacity 0.25s ease;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#leaderboardSection.hidden {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#leaderboardSection.visible {
|
||||
max-width: 100%; /* take full available space in grid column */
|
||||
opacity: 1;
|
||||
}
|
||||
#rankingExplanation {
|
||||
transition: all 0.35s ease;
|
||||
}
|
||||
|
||||
/* Pagination Controls */
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.pagination-btn {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: var(--hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pagination-info {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Hide pagination when not needed */
|
||||
.pagination-controls.hidden {
|
||||
display: none;
|
||||
}
|
||||
296
src/static/problem.css
vendored
Normal file
296
src/static/problem.css
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
:root {
|
||||
--bg: #f9f9f9;
|
||||
--card: #fff;
|
||||
--text: #333;
|
||||
--muted: #666;
|
||||
--accent: #007bff;
|
||||
--accent-hover: #0069d9;
|
||||
--border: #eaeaea;
|
||||
--hover: #f8f9fa;
|
||||
--code-bg: #f6f8fa;
|
||||
--editor-border: #ddd;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--border: #334155;
|
||||
--hover: #334155;
|
||||
--code-bg: #1e293b;
|
||||
--editor-border: #475569;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh; /* allow content to grow */
|
||||
overflow-y: auto; /* allow vertical scroll */
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* wrap on small screens */
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.problem-panel {
|
||||
flex: 1 1 400px; /* grow/shrink with base 400px */
|
||||
min-width: 300px;
|
||||
background: var(--card);
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
border-right: 1px solid var(--border);
|
||||
max-height: 100vh;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1 1 400px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--card);
|
||||
max-height: 100vh;
|
||||
overflow: hidden; /* internal scroll handling */
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.problem-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: var(--muted);
|
||||
margin-right: 15px;
|
||||
padding: 6px 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.dark-mode-toggle {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-mode-toggle:hover {
|
||||
background: var(--hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
html.dark .dark-mode-icon::before {
|
||||
content: "☀";
|
||||
}
|
||||
|
||||
html:not(.dark) .dark-mode-icon::before {
|
||||
content: "⏾";
|
||||
}
|
||||
|
||||
.dark-mode-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dark-mode-icon::before {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.problem-desc {
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.problem-desc pre {
|
||||
background: var(--code-bg);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.problem-desc code {
|
||||
background: var(--code-bg);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
padding: 15px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-actions button {
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-actions button:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1 1 auto;
|
||||
min-height: 300px;
|
||||
border: 1px solid var(--editor-border);
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: var(--hover);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 120px;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
border: 1px solid var(--border);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.result-panel h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-panel pre {
|
||||
background: var(--code-bg);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
font-family: "Inter", sans-serif;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.problem-panel,
|
||||
.editor-container {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
max-height: none;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
#editor {
|
||||
min-height: 400px;
|
||||
max-height: none;
|
||||
}
|
||||
.result-panel {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
577
static/style.css → src/static/style.css
vendored
577
static/style.css → src/static/style.css
vendored
@@ -1,260 +1,317 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--bg: #f8f9fa;
|
||||
--card: #fff;
|
||||
--text: #333;
|
||||
--heading: #2c3e50;
|
||||
--heading-secondary: #34495e;
|
||||
--accent: #3498db;
|
||||
--accent-hover: #2980b9;
|
||||
--success: #27ae60;
|
||||
--success-hover: #229954;
|
||||
--error: #e74c3c;
|
||||
--muted: #6c757d;
|
||||
--muted-hover: #5a6268;
|
||||
--border: #ddd;
|
||||
--code-bg: #f4f4f4;
|
||||
--success-bg: #d4edda;
|
||||
--success-text: #155724;
|
||||
--error-bg: #f8d7da;
|
||||
--error-text: #721c24;
|
||||
--hover-bg: #e3f2fd;
|
||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--heading: #3b82f6;
|
||||
--heading-secondary: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--success: #22c55e;
|
||||
--success-hover: #16a34a;
|
||||
--error: #ef4444;
|
||||
--muted: #64748b;
|
||||
--muted-hover: #475569;
|
||||
--border: #334155;
|
||||
--code-bg: #1e293b;
|
||||
--success-bg: #065f46;
|
||||
--success-text: #d1fae5;
|
||||
--error-bg: #7f1d1d;
|
||||
--error-text: #fecaca;
|
||||
--hover-bg: #1e40af;
|
||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 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: var(--text);
|
||||
background-color: var(--bg);
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Main heading */
|
||||
h1 {
|
||||
color: var(--heading);
|
||||
margin-bottom: -10px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 3px solid var(--accent);
|
||||
font-size: 2.2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--heading-secondary);
|
||||
margin: 30px 0 20px 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--heading-secondary);
|
||||
margin: 25px 0 15px 0;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
/* Links and buttons */
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: var(--hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Primary action link (Submit New Problem) */
|
||||
a[href="/problem/new"] {
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
margin-bottom: 30px;
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
a[href="/problem/new"]:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Problem list */
|
||||
ul {
|
||||
list-style: none;
|
||||
background: var(--card);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
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: var(--hover-bg);
|
||||
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: var(--muted);
|
||||
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: var(--muted-hover);
|
||||
}
|
||||
|
||||
.problem-desc {
|
||||
background: var(--card);
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.7;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Editor section */
|
||||
.editor-section {
|
||||
background: var(--card);
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 30px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#editor {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
form button[type="submit"] {
|
||||
background-color: var(--success);
|
||||
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: var(--success-hover);
|
||||
}
|
||||
|
||||
/* Results section */
|
||||
b {
|
||||
color: var(--heading);
|
||||
display: inline-block;
|
||||
margin: 10px 0 5px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--code-bg);
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--accent);
|
||||
margin: 10px 0 20px 0;
|
||||
overflow-x: auto;
|
||||
font-family: "JetBrains Mono", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
border: 1px solid var(--border);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
pre[style*="color:red"] {
|
||||
border-left-color: var(--error);
|
||||
background-color: var(--error-bg);
|
||||
}
|
||||
|
||||
/* Status messages */
|
||||
p[style*="color:green"] {
|
||||
background-color: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
padding: 15px 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--success);
|
||||
margin: 20px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p[style*="color:red"] {
|
||||
background-color: var(--error-bg);
|
||||
color: var(--error-text);
|
||||
padding: 15px 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid var(--error);
|
||||
margin: 20px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Back to Problems link */
|
||||
a[href="/"] {
|
||||
display: inline-block;
|
||||
margin-top: 30px;
|
||||
background-color: var(--muted);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a[href="/"]:hover {
|
||||
background-color: var(--muted-hover);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.problem-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.problem-desc,
|
||||
.editor-section,
|
||||
ul {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
166
src/templates/index.html
Normal file
166
src/templates/index.html
Normal file
@@ -0,0 +1,166 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="">
|
||||
<head>
|
||||
<script>
|
||||
/*
|
||||
This fix is for the fucking epileptics. idk what they're called.
|
||||
It fixes the fucking flashing between changing pages.
|
||||
This is both in the problems and this file
|
||||
---
|
||||
This changes nothing if the user uses light-system
|
||||
*/
|
||||
(function() {
|
||||
try {
|
||||
var dark = localStorage.getItem("darkMode");
|
||||
if (
|
||||
dark === "true" ||
|
||||
(dark === null && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Quick Problem Platform</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
|
||||
</head>
|
||||
<style>
|
||||
/* Popout explanation */
|
||||
#rankingExplanation {
|
||||
grid-column: 1 / span 2;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease, opacity 0.4s ease, padding 0.4s ease;
|
||||
padding: 0 12px;
|
||||
}
|
||||
#rankingExplanation.active {
|
||||
max-height: 800px;
|
||||
opacity: 1;
|
||||
padding: 12px;
|
||||
}
|
||||
#rankInfoBtn.active { color: #2563eb; cursor:pointer; transition: transform 0.3s ease; }
|
||||
#rankInfoBtn.active { transform: rotate(90deg); }
|
||||
|
||||
/* Highlight top rank */
|
||||
.rank-1 td:first-child { font-weight: bold; }
|
||||
.sort-asc::after { content: " ↑"; }
|
||||
.sort-desc::after { content: " ↓"; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<h1>Quick Problem Platform</h1>
|
||||
<button id="darkModeToggle" class="dark-mode-toggle" title="Toggle dark mode">
|
||||
<span class="dark-mode-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content" id="contentContainer">
|
||||
<!-- Problems -->
|
||||
<section class="card problems-list">
|
||||
<div class="search-controls">
|
||||
<input type="text" class="search-input" id="problemSearch" placeholder="Search problems..." />
|
||||
</div>
|
||||
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
|
||||
<div id="problemsContainer">
|
||||
{% for folder, description, test_code, difficulty in problems %}
|
||||
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}" data-difficulty="{{ difficulty|lower }}">
|
||||
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
|
||||
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="problem-item">No problems yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="pagination-controls" id="problemsPagination">
|
||||
<button class="pagination-btn" id="problemsPrevBtn" disabled>← Previous</button>
|
||||
<span class="pagination-info" id="problemsPaginationInfo">Page 1 of 1</span>
|
||||
<button class="pagination-btn" id="problemsNextBtn" disabled>Next →</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<section class="card" id="leaderboardSection">
|
||||
<div class="leaderboard-head">
|
||||
<h2 style="font-size:1.1rem;margin:0">Leaderboard
|
||||
<!--<span id="rankInfoBtn" title="How ranking works">ℹ️</span>-->
|
||||
</h2>
|
||||
</div>
|
||||
<div class="leaderboard-controls">
|
||||
<input type="text" class="search-input" id="problemFilter" placeholder="Filter by problem..." />
|
||||
<select class="filter-select" id="runtimeFilter">
|
||||
<option value="">All runtimes</option>
|
||||
<option value="best">Best runtime</option>
|
||||
<option value="worst">Worst runtime</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="leaderboardContainer">
|
||||
<table class="leaderboard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" data-sort="rank">Rank</th>
|
||||
<th class="sortable" data-sort="user">User</th>
|
||||
<th class="sortable" data-sort="problem">Problem</th>
|
||||
<th class="sortable" data-sort="runtime">Runtime (s)</th>
|
||||
<th class="sortable" data-sort="memory">Memory (KB)</th>
|
||||
<th>Line</th>
|
||||
<th class="sortable" data-sort="timestamp">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="leaderboardBody">
|
||||
{% for entry in leaderboard %}
|
||||
<tr data-user="{{ entry[0] }}" data-problem="{{ problem_titles.get(entry[1], 'Unknown') }}"
|
||||
data-runtime="{{ '%.4f'|format(entry[2]) }}" data-memory="{{ entry[3] }}"
|
||||
data-timestamp="{{ entry[5] }}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ entry[0] }}</td>
|
||||
<td>
|
||||
<a href="/problem/{{ problem_titles.get(entry[1], 'Unknown') }}"
|
||||
style="color:#2563eb; text-decoration: none;"
|
||||
onmouseover="this.style.textDecoration='underline';"
|
||||
onmouseout="this.style.textDecoration='none';">
|
||||
{{ problem_titles.get(entry[1], 'Unknown') }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ '%.4f'|format(entry[2]) }}</td>
|
||||
<td>{{ entry[3] }}</td>
|
||||
<td>{{ entry[4] if entry[4] else '-' }}</td>
|
||||
<td>{{ entry[5] }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7">No leaderboard entries yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ranking explanation -->
|
||||
<section class="card" id="rankingExplanation">
|
||||
<h2 style="font-size:1.1rem;margin-bottom:6px">How Ranking Works</h2>
|
||||
<p>The leaderboard uses a <strong>weighted scoring system</strong> to determine overall rank:</p>
|
||||
<ul style="margin-left: 15px;">
|
||||
<li><strong>Runtime:</strong> How fast the solution runs (lower is better).</li>
|
||||
<li><strong>Memory Usage:</strong> How much memory the solution uses (lower is better).</li>
|
||||
</ul>
|
||||
<p>Overall score is calculated as:</p>
|
||||
<div style="background:#f9f9f9; border-left:4px solid #007acc; padding:1rem; margin:1rem 0;">
|
||||
<code>
|
||||
runtimeScore = yourRuntime / bestRuntime<br>
|
||||
memoryScore = yourMemory / bestMemory<br>
|
||||
overallScore = runtimeScore × 0.7 + memoryScore × 0.3
|
||||
</code>
|
||||
</div>
|
||||
<p>Lower overall scores are better. If scores are equal, earlier submission ranks higher.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('serve_js', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
178
src/templates/problem.html
Normal file
178
src/templates/problem.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="">
|
||||
<head>
|
||||
<script>
|
||||
/*
|
||||
This fix is for the fucking epileptics. idk what they're called.
|
||||
It fixes the fucking flashing between changing pages.
|
||||
This is both in the index and this file
|
||||
---
|
||||
This changes nothing if the user uses light-system
|
||||
*/
|
||||
(function() {
|
||||
try {
|
||||
var dark = localStorage.getItem("darkMode");
|
||||
if (
|
||||
dark === "true" ||
|
||||
(dark === null && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ problem.title }} - Coding Problem</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<link rel="stylesheet" href="/static/problem.css" />
|
||||
<!-- this is stoopid fucking html link for favicon. just cause of flask-->
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.cdnfonts.com/css/jetbrains-mono"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div class="problem-panel">
|
||||
<div class="problem-header">
|
||||
<button class="back-btn" onclick="window.location.href='/'">
|
||||
← Back
|
||||
</button>
|
||||
<h1>{{ problem.title }}</h1>
|
||||
<button
|
||||
id="darkModeToggle"
|
||||
class="dark-mode-toggle"
|
||||
title="Toggle dark mode"
|
||||
>
|
||||
<span class="dark-mode-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="problem-desc">
|
||||
{{ problem.description | safe | markdown }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div class="editor-header">
|
||||
<h2 style="margin: 0; font-size: 18px">
|
||||
Submit Your Solution (Python)
|
||||
</h2>
|
||||
</div>
|
||||
<div class="editor-wrapper">
|
||||
<form method="post">
|
||||
<label for="username">Username (optional):</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Anonymous"
|
||||
/>
|
||||
<div id="editor"></div>
|
||||
<textarea
|
||||
name="user_code"
|
||||
id="user_code"
|
||||
style="display: none"
|
||||
></textarea>
|
||||
<div class="editor-actions">
|
||||
<button type="submit">Run & Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="result-panel">
|
||||
<h3>Result</h3>
|
||||
{% if result %}
|
||||
<p>
|
||||
<b>Runtime:</b> {{ '%.4f'|format(result.runtime) }}
|
||||
seconds
|
||||
</p>
|
||||
<p><b>Output:</b></p>
|
||||
<pre>{{ result.output }}</pre>
|
||||
{% if result.error %}
|
||||
<p><b>Error:</b></p>
|
||||
<pre>{{ result.error }}</pre>
|
||||
{% endif %} {% else %}
|
||||
<div class="placeholder">
|
||||
Your code execution results will appear here
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
<script>
|
||||
// Dark mode functionality
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const html = document.documentElement;
|
||||
|
||||
// Load saved dark mode preference
|
||||
const savedDarkMode = localStorage.getItem("darkMode");
|
||||
if (
|
||||
savedDarkMode === "true" ||
|
||||
(savedDarkMode === null &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
html.classList.add("dark");
|
||||
}
|
||||
|
||||
darkModeToggle.addEventListener("click", () => {
|
||||
html.classList.toggle("dark");
|
||||
localStorage.setItem(
|
||||
"darkMode",
|
||||
html.classList.contains("dark"),
|
||||
);
|
||||
// Update Monaco editor theme
|
||||
if (window.monacoEditor) {
|
||||
const isDark = html.classList.contains("dark");
|
||||
window.monacoEditor.updateOptions({
|
||||
theme: isDark ? "vs-dark" : "vs-light",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs",
|
||||
},
|
||||
});
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
const isDark = html.classList.contains("dark");
|
||||
window.monacoEditor = monaco.editor.create(
|
||||
document.getElementById("editor"),
|
||||
{
|
||||
value: "",
|
||||
language: "python",
|
||||
theme: isDark ? "vs-dark" : "vs-light",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
fontLigatures: true,
|
||||
automaticLayout: true,
|
||||
fontSize: 16,
|
||||
minimap: { enabled: false },
|
||||
},
|
||||
);
|
||||
document
|
||||
.querySelector("form")
|
||||
.addEventListener("submit", function (e) {
|
||||
var code = window.monacoEditor.getValue();
|
||||
if (!code.trim()) {
|
||||
alert("Please enter your code before submitting.");
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
document.getElementById("user_code").value = code;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
591
src/utils.py
Normal file
591
src/utils.py
Normal file
@@ -0,0 +1,591 @@
|
||||
import sys
|
||||
import traceback
|
||||
import time
|
||||
import io
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import ast
|
||||
import signal
|
||||
import resource
|
||||
import shlex
|
||||
import hashlib
|
||||
import platform
|
||||
from contextlib import contextmanager
|
||||
|
||||
# Security configuration - Expanded whitelist
|
||||
ALLOWED_IMPORTS = {
|
||||
'math', 'random', 'datetime', 'json', 'collections', 'itertools',
|
||||
'functools', 'operator', 'copy', 'unittest', 're', 'string', 'pyfiglet',
|
||||
'decimal', 'fractions', 'statistics', 'textwrap', 'unicodedata',
|
||||
'base64', 'binascii', 'struct', 'array', 'heapq', 'bisect'
|
||||
}
|
||||
|
||||
# Enhanced dangerous patterns with more comprehensive coverage
|
||||
DANGEROUS_PATTERNS = [
|
||||
# System/OS operations
|
||||
r'import\s+os(?:\s|$|\.)', r'from\s+os\s+import',
|
||||
r'import\s+subprocess(?:\s|$|\.)', r'from\s+subprocess\s+import',
|
||||
r'import\s+sys(?:\s|$|\.)', r'from\s+sys\s+import',
|
||||
r'import\s+shutil(?:\s|$|\.)', r'from\s+shutil\s+import',
|
||||
r'import\s+pathlib(?:\s|$|\.)', r'from\s+pathlib\s+import',
|
||||
r'import\s+tempfile(?:\s|$|\.)', r'from\s+tempfile\s+import',
|
||||
r'import\s+glob(?:\s|$|\.)', r'from\s+glob\s+import',
|
||||
r'import\s+platform(?:\s|$|\.)', r'from\s+platform\s+import',
|
||||
|
||||
# Network operations
|
||||
r'import\s+socket(?:\s|$|\.)', r'from\s+socket\s+import',
|
||||
r'import\s+urllib(?:\s|$|\.)', r'from\s+urllib\s+import',
|
||||
r'import\s+requests(?:\s|$|\.)', r'from\s+requests\s+import',
|
||||
r'import\s+http(?:\s|$|\.)', r'from\s+http\s+import',
|
||||
r'import\s+ftplib(?:\s|$|\.)', r'from\s+ftplib\s+import',
|
||||
r'import\s+smtplib(?:\s|$|\.)', r'from\s+smtplib\s+import',
|
||||
|
||||
# Dynamic execution
|
||||
r'__import__\s*\(', r'exec\s*\(', r'eval\s*\(', r'compile\s*\(',
|
||||
r'globals\s*\(', r'locals\s*\(', r'vars\s*\(', r'dir\s*\(',
|
||||
r'getattr\s*\(', r'setattr\s*\(', r'delattr\s*\(', r'hasattr\s*\(',
|
||||
|
||||
# File operations
|
||||
r'open\s*\(', r'file\s*\(', r'input\s*\(', r'raw_input\s*\(',
|
||||
|
||||
# Destructive operations
|
||||
r'\.unlink\s*\(', r'\.remove\s*\(', r'\.rmdir\s*\(', r'\.rmtree\s*\(',
|
||||
r'\.delete\s*\(', r'\.kill\s*\(', r'\.terminate\s*\(',
|
||||
|
||||
# Threading and multiprocessing
|
||||
r'import\s+threading(?:\s|$|\.)', r'from\s+threading\s+import',
|
||||
r'import\s+multiprocessing(?:\s|$|\.)', r'from\s+multiprocessing\s+import',
|
||||
r'import\s+asyncio(?:\s|$|\.)', r'from\s+asyncio\s+import',
|
||||
|
||||
# Memory and resource manipulation
|
||||
r'import\s+gc(?:\s|$|\.)', r'from\s+gc\s+import',
|
||||
r'import\s+resource(?:\s|$|\.)', r'from\s+resource\s+import',
|
||||
r'import\s+ctypes(?:\s|$|\.)', r'from\s+ctypes\s+import',
|
||||
|
||||
# Code introspection
|
||||
r'import\s+inspect(?:\s|$|\.)', r'from\s+inspect\s+import',
|
||||
r'import\s+types(?:\s|$|\.)', r'from\s+types\s+import',
|
||||
|
||||
# Pickle and serialization security risks
|
||||
r'import\s+pickle(?:\s|$|\.)', r'from\s+pickle\s+import',
|
||||
r'import\s+marshal(?:\s|$|\.)', r'from\s+marshal\s+import',
|
||||
|
||||
# System exit
|
||||
r'exit\s*\(', r'quit\s*\(', r'sys\.exit\s*\(',
|
||||
|
||||
# Dunder methods are dangerous if misused, for us we allow classes
|
||||
# specifically the constructor
|
||||
# del i dont allow tho
|
||||
r'__del__\s*\(',
|
||||
|
||||
# Import tricks
|
||||
r'importlib', r'imp\s', r'pkgutil',
|
||||
]
|
||||
|
||||
# Maximum resource limits
|
||||
MAX_MEMORY_MB = 100 # 100MB memory limit
|
||||
MAX_CPU_TIME = 5 # 5 seconds CPU time
|
||||
MAX_OUTPUT_SIZE = 10000 # 10KB output limit
|
||||
MAX_CODE_SIZE = 50000 # 50KB code limit
|
||||
MAX_TEST_SIZE = 10000 # 10KB test limit
|
||||
|
||||
class SecurityViolationError(Exception):
|
||||
"""Raised when a security violation is detected."""
|
||||
pass
|
||||
|
||||
class ResourceLimitError(Exception):
|
||||
"""Raised when resource limits are exceeded."""
|
||||
pass
|
||||
|
||||
@contextmanager
|
||||
def resource_limits():
|
||||
"""Context manager to set resource limits."""
|
||||
# Set memory limit (in bytes)
|
||||
if hasattr(resource, 'RLIMIT_AS'):
|
||||
try:
|
||||
resource.setrlimit(resource.RLIMIT_AS, (MAX_MEMORY_MB * 1024 * 1024, MAX_MEMORY_MB * 1024 * 1024))
|
||||
except (OSError, ValueError):
|
||||
pass # Ignore if we can't set memory limits
|
||||
|
||||
# Set CPU time limit
|
||||
if hasattr(resource, 'RLIMIT_CPU'):
|
||||
try:
|
||||
resource.setrlimit(resource.RLIMIT_CPU, (MAX_CPU_TIME, MAX_CPU_TIME))
|
||||
except (OSError, ValueError):
|
||||
pass # Ignore if we can't set CPU limits
|
||||
|
||||
# Set file descriptor limit
|
||||
if hasattr(resource, 'RLIMIT_NOFILE'):
|
||||
try:
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (10, 10))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Reset limits (though this won't matter much in subprocess)
|
||||
pass
|
||||
|
||||
def validate_code_security(code):
|
||||
"""
|
||||
Enhanced security validation for code.
|
||||
Returns (is_safe, error_message)
|
||||
"""
|
||||
if not isinstance(code, str):
|
||||
return False, "Code must be a string"
|
||||
|
||||
if len(code.strip()) == 0:
|
||||
return False, "Code cannot be empty"
|
||||
|
||||
# Check code size limits
|
||||
if len(code) > MAX_CODE_SIZE:
|
||||
return False, f"Code too large (maximum {MAX_CODE_SIZE} bytes allowed)"
|
||||
|
||||
# Check for null bytes and other binary content
|
||||
if '\x00' in code:
|
||||
return False, "Code contains null bytes"
|
||||
|
||||
# Check for dangerous patterns with case-insensitive matching
|
||||
for pattern in DANGEROUS_PATTERNS:
|
||||
matches = re.findall(pattern, code, re.IGNORECASE | re.MULTILINE)
|
||||
if matches:
|
||||
return False, f"Dangerous operation detected: {pattern} (matched: {matches[0] if matches else 'unknown'})"
|
||||
|
||||
# Check for excessive nesting (possible DoS)
|
||||
nesting_level = 0
|
||||
max_nesting = 20
|
||||
for char in code:
|
||||
if char in '([{':
|
||||
nesting_level += 1
|
||||
if nesting_level > max_nesting:
|
||||
return False, f"Excessive nesting detected (max {max_nesting} levels)"
|
||||
elif char in ')]}':
|
||||
nesting_level = max(0, nesting_level - 1)
|
||||
|
||||
# Parse AST with enhanced validation
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
|
||||
# Check for dangerous AST nodes
|
||||
for node in ast.walk(tree):
|
||||
# Import validation
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
module_name = alias.name.split('.')[0]
|
||||
if module_name not in ALLOWED_IMPORTS:
|
||||
return False, f"Import not allowed: {module_name}"
|
||||
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
module_name = node.module.split('.')[0]
|
||||
if module_name not in ALLOWED_IMPORTS:
|
||||
return False, f"Import not allowed: {module_name}"
|
||||
|
||||
# Check for attribute access on dangerous modules
|
||||
elif isinstance(node, ast.Attribute):
|
||||
if hasattr(node.value, 'id') and node.value.id in ['os', 'sys', 'subprocess']:
|
||||
return False, f"Dangerous attribute access: {node.value.id}.{node.attr}"
|
||||
|
||||
# Check for function calls that might be dangerous
|
||||
elif isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name):
|
||||
if node.func.id in ['exec', 'eval', 'compile', '__import__', 'open', 'input']:
|
||||
return False, f"Dangerous function call: {node.func.id}"
|
||||
elif isinstance(node.func, ast.Attribute):
|
||||
if node.func.attr in ['system', 'popen', 'spawn', 'fork']:
|
||||
return False, f"Dangerous method call: {node.func.attr}"
|
||||
|
||||
# Check for while True loops without breaks (potential infinite loops)
|
||||
elif isinstance(node, ast.While):
|
||||
if isinstance(node.test, ast.Constant) and node.test.value is True:
|
||||
# Check if there's a break statement in the loop
|
||||
has_break = any(isinstance(n, ast.Break) for n in ast.walk(node))
|
||||
if not has_break:
|
||||
return False, "Potentially infinite loop detected (while True without break)"
|
||||
|
||||
except SyntaxError as e:
|
||||
return False, f"Syntax error in code: {str(e)}"
|
||||
except RecursionError:
|
||||
return False, "Code too complex (recursion limit exceeded during parsing)"
|
||||
except Exception as e:
|
||||
return False, f"Code validation error: {str(e)}"
|
||||
|
||||
return True, None
|
||||
|
||||
def create_restricted_globals():
|
||||
"""Create a heavily restricted global namespace for code execution."""
|
||||
# Very limited set of safe builtins
|
||||
safe_builtins = {
|
||||
'abs', 'all', 'any', 'bin', 'bool', 'chr', 'dict', 'enumerate',
|
||||
'filter', 'float', 'format', 'frozenset', 'hex', 'int', 'isinstance',
|
||||
'issubclass', 'iter', 'len', 'list', 'map', 'max', 'min', 'next',
|
||||
'oct', 'ord', 'pow', 'print', 'range', 'repr', 'reversed', 'round',
|
||||
'set', 'slice', 'sorted', 'str', 'sum', 'tuple', 'type', 'zip'
|
||||
}
|
||||
|
||||
# Create restricted builtins dict with error-raising versions of dangerous functions
|
||||
restricted_builtins = {}
|
||||
for name in safe_builtins:
|
||||
if name in __builtins__ if isinstance(__builtins__, dict) else dir(__builtins__):
|
||||
if isinstance(__builtins__, dict):
|
||||
restricted_builtins[name] = __builtins__[name]
|
||||
else:
|
||||
restricted_builtins[name] = getattr(__builtins__, name)
|
||||
|
||||
# Add error-raising versions of dangerous functions
|
||||
def raise_security_error(name):
|
||||
def _error(*args, **kwargs):
|
||||
raise SecurityViolationError(f"Access to '{name}' is not permitted")
|
||||
return _error
|
||||
|
||||
dangerous_builtins = ['exec', 'eval', 'compile', '__import__', 'open', 'input', 'globals', 'locals', 'vars']
|
||||
for name in dangerous_builtins:
|
||||
restricted_builtins[name] = raise_security_error(name)
|
||||
|
||||
restricted_globals = {
|
||||
'__builtins__': restricted_builtins,
|
||||
'__name__': '__restricted__',
|
||||
'__doc__': None,
|
||||
}
|
||||
|
||||
# Add allowed modules with error handling
|
||||
for module in ALLOWED_IMPORTS:
|
||||
try:
|
||||
imported_module = __import__(module)
|
||||
restricted_globals[module] = imported_module
|
||||
except ImportError:
|
||||
pass # Module not available, skip
|
||||
|
||||
return restricted_globals
|
||||
|
||||
def create_secure_temp_environment():
|
||||
"""Create a secure temporary directory with restricted permissions."""
|
||||
temp_dir = tempfile.mkdtemp(prefix='secure_code_exec_')
|
||||
|
||||
# Set restrictive permissions on the directory
|
||||
try:
|
||||
os.chmod(temp_dir, 0o700) # Only owner can read/write/execute
|
||||
except OSError:
|
||||
pass # Best effort
|
||||
|
||||
return temp_dir
|
||||
|
||||
def cleanup_temp_environment(temp_dir):
|
||||
"""Securely clean up temporary directory and all contents."""
|
||||
if not temp_dir or not os.path.exists(temp_dir):
|
||||
return
|
||||
|
||||
try:
|
||||
# Recursively remove all files and subdirectories
|
||||
for root, dirs, files in os.walk(temp_dir, topdown=False):
|
||||
for name in files:
|
||||
file_path = os.path.join(root, name)
|
||||
try:
|
||||
os.chmod(file_path, 0o600) # Ensure we can delete
|
||||
os.unlink(file_path)
|
||||
except OSError:
|
||||
pass
|
||||
for name in dirs:
|
||||
dir_path = os.path.join(root, name)
|
||||
try:
|
||||
os.chmod(dir_path, 0o700) # Ensure we can delete
|
||||
os.rmdir(dir_path)
|
||||
except OSError:
|
||||
pass
|
||||
os.rmdir(temp_dir)
|
||||
except Exception as e:
|
||||
# Log warning but don't fail
|
||||
print(f"Warning: Could not fully clean up temp directory {temp_dir}: {e}", file=sys.stderr)
|
||||
|
||||
def run_code_against_tests(user_code, test_code, max_execution_time=5):
|
||||
"""
|
||||
Securely run user code against test code with enhanced safety restrictions.
|
||||
|
||||
Args:
|
||||
user_code: The user's solution code
|
||||
test_code: The test code to validate the solution
|
||||
max_execution_time: Maximum execution time in seconds (default: 5)
|
||||
|
||||
Returns:
|
||||
dict: Result containing passed, output, runtime, and error information
|
||||
"""
|
||||
# Input validation
|
||||
if not isinstance(user_code, str) or not isinstance(test_code, str):
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': "Both user_code and test_code must be strings"
|
||||
}
|
||||
|
||||
# Validate execution time limit
|
||||
max_execution_time = min(max(1, int(max_execution_time)), MAX_CPU_TIME)
|
||||
|
||||
# Enhanced security validation
|
||||
user_safe, user_error = validate_code_security(user_code)
|
||||
if not user_safe:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"Security violation in user code: {user_error}"
|
||||
}
|
||||
|
||||
test_safe, test_error = validate_code_security(test_code)
|
||||
if not test_safe:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"Security violation in test code: {test_error}"
|
||||
}
|
||||
|
||||
# Additional test code size validation
|
||||
if len(test_code) > MAX_TEST_SIZE:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"Test code too large (maximum {MAX_TEST_SIZE} bytes allowed)"
|
||||
}
|
||||
|
||||
local_ns = {}
|
||||
output = ''
|
||||
start = time.perf_counter()
|
||||
error = None
|
||||
passed = False
|
||||
temp_dir = None
|
||||
|
||||
try:
|
||||
# Check if unittest is used in test_code
|
||||
if 'unittest' in test_code:
|
||||
# Create secure temp environment
|
||||
temp_dir = create_secure_temp_environment()
|
||||
temp_file = os.path.join(temp_dir, 'test_code.py')
|
||||
|
||||
try:
|
||||
combined_code = f"{user_code}\n\n{test_code}"
|
||||
|
||||
# Write to temp file with restricted permissions
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write(combined_code)
|
||||
os.chmod(temp_file, 0o600) # Read/write for owner only
|
||||
|
||||
# Prepare secure environment variables
|
||||
secure_env = {
|
||||
'PYTHONPATH': '',
|
||||
'PYTHONDONTWRITEBYTECODE': '1',
|
||||
'PYTHONUNBUFFERED': '1',
|
||||
'PATH': '/usr/bin:/bin', # Minimal PATH
|
||||
}
|
||||
|
||||
# Add current Python executable path if needed
|
||||
python_dir = os.path.dirname(sys.executable)
|
||||
if python_dir not in secure_env['PATH']:
|
||||
secure_env['PATH'] = f"{python_dir}:{secure_env['PATH']}"
|
||||
|
||||
# Run with subprocess and comprehensive security measures
|
||||
try:
|
||||
# Create a wrapper script for additional security
|
||||
wrapper_code = f"""
|
||||
import sys
|
||||
import signal
|
||||
import resource
|
||||
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("Execution timed out")
|
||||
|
||||
# Set up timeout handler
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm({max_execution_time})
|
||||
|
||||
try:
|
||||
# Set resource limits
|
||||
{resource_limits.__code__.co_consts}
|
||||
with resource_limits():
|
||||
exec(open(r'{temp_file}').read())
|
||||
except Exception as e:
|
||||
print(f"Error: {{e}}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
"""
|
||||
|
||||
wrapper_file = os.path.join(temp_dir, 'wrapper.py')
|
||||
with open(wrapper_file, 'w', encoding='utf-8') as f:
|
||||
f.write(wrapper_code)
|
||||
os.chmod(wrapper_file, 0o600)
|
||||
|
||||
# Use the more secure wrapper approach
|
||||
proc = subprocess.run(
|
||||
[sys.executable, temp_file], # Direct execution for now
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=max_execution_time + 1, # Add buffer for subprocess overhead
|
||||
encoding='utf-8',
|
||||
cwd=temp_dir,
|
||||
env=secure_env,
|
||||
# Additional security on Unix systems
|
||||
preexec_fn=os.setpgrp if hasattr(os, 'setpgrp') else None
|
||||
)
|
||||
|
||||
# Process output
|
||||
combined_output = ""
|
||||
if proc.stdout:
|
||||
combined_output += proc.stdout
|
||||
if proc.stderr:
|
||||
if combined_output:
|
||||
combined_output += "\n" + proc.stderr
|
||||
else:
|
||||
combined_output = proc.stderr
|
||||
|
||||
# Limit output size
|
||||
if len(combined_output) > MAX_OUTPUT_SIZE:
|
||||
combined_output = combined_output[:MAX_OUTPUT_SIZE] + "\n... (output truncated)"
|
||||
|
||||
output = combined_output
|
||||
passed = proc.returncode == 0
|
||||
|
||||
if not passed and proc.returncode != 0:
|
||||
error = f"Tests failed. Return code: {proc.returncode}"
|
||||
if output.strip():
|
||||
error += f"\nOutput: {output}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
passed = False
|
||||
error = f"Code execution timed out after {max_execution_time} seconds"
|
||||
output = "Execution timed out"
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Subprocess execution error: {str(e)}"
|
||||
|
||||
finally:
|
||||
# Secure cleanup
|
||||
cleanup_temp_environment(temp_dir)
|
||||
|
||||
else:
|
||||
# Direct execution with heavily restricted globals
|
||||
old_stdout = sys.stdout
|
||||
captured_output = io.StringIO()
|
||||
sys.stdout = captured_output
|
||||
|
||||
try:
|
||||
# Create restricted execution environment
|
||||
restricted_globals = create_restricted_globals()
|
||||
|
||||
# Set up timeout for direct execution
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("Execution timed out")
|
||||
|
||||
if hasattr(signal, 'SIGALRM'):
|
||||
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(max_execution_time)
|
||||
|
||||
try:
|
||||
# Execute user code in restricted environment
|
||||
exec(user_code, restricted_globals, local_ns)
|
||||
|
||||
# Execute test code
|
||||
exec(test_code, {**restricted_globals, **local_ns}, local_ns)
|
||||
passed = True
|
||||
|
||||
finally:
|
||||
if hasattr(signal, 'SIGALRM'):
|
||||
signal.alarm(0) # Cancel alarm
|
||||
signal.signal(signal.SIGALRM, old_handler) # Restore handler
|
||||
|
||||
except TimeoutError:
|
||||
passed = False
|
||||
error = f"Code execution timed out after {max_execution_time} seconds"
|
||||
except SecurityViolationError as e:
|
||||
passed = False
|
||||
error = f"Security violation: {str(e)}"
|
||||
except AssertionError as e:
|
||||
passed = False
|
||||
error = f"Assertion failed: {str(e)}"
|
||||
except MemoryError:
|
||||
passed = False
|
||||
error = "Memory limit exceeded"
|
||||
except RecursionError:
|
||||
passed = False
|
||||
error = "Maximum recursion depth exceeded"
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Runtime error: {str(e)}"
|
||||
# Don't include full traceback for security
|
||||
finally:
|
||||
output = captured_output.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
# Limit output size
|
||||
if len(output) > MAX_OUTPUT_SIZE:
|
||||
output = output[:MAX_OUTPUT_SIZE] + "\n... (output truncated)"
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Execution error: {str(e)}"
|
||||
if temp_dir:
|
||||
cleanup_temp_environment(temp_dir)
|
||||
|
||||
runtime = time.perf_counter() - start
|
||||
|
||||
result = {
|
||||
'passed': passed,
|
||||
'output': output.strip() if output else '',
|
||||
'runtime': runtime,
|
||||
'error': error if not passed else None
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def safe_code_runner(user_code, test_code):
|
||||
"""
|
||||
Enhanced safety wrapper with comprehensive security checks.
|
||||
"""
|
||||
# Input validation
|
||||
if not isinstance(user_code, str) or not isinstance(test_code, str):
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': "Both user_code and test_code must be strings"
|
||||
}
|
||||
|
||||
# Enhanced length checks
|
||||
if len(user_code) > MAX_CODE_SIZE:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"User code too large (maximum {MAX_CODE_SIZE} bytes allowed)"
|
||||
}
|
||||
|
||||
if len(test_code) > MAX_TEST_SIZE:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"Test code too large (maximum {MAX_TEST_SIZE} bytes allowed)"
|
||||
}
|
||||
|
||||
# Check for empty code
|
||||
if not user_code.strip():
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': "User code cannot be empty"
|
||||
}
|
||||
|
||||
if not test_code.strip():
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': "Test code cannot be empty"
|
||||
}
|
||||
|
||||
return run_code_against_tests(user_code, test_code, MAX_CPU_TIME)
|
||||
160
static/index.css
vendored
160
static/index.css
vendored
@@ -1,160 +0,0 @@
|
||||
:root {
|
||||
--bg: #f6f8fb;
|
||||
--card: #fff;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb;
|
||||
--shadow: 0 4px 12px rgba(16, 24, 40, 0.06);
|
||||
--radius: 8px;
|
||||
--mono: 'JetBrains Mono', monospace;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
background: var(--bg);
|
||||
color: #0f172a;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.wrap {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 1.6rem;
|
||||
color: #111827;
|
||||
}
|
||||
header p {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.content.single-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 12px;
|
||||
}
|
||||
/* Search/filter controls */
|
||||
.search-controls {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.filter-select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background: white;
|
||||
}
|
||||
/* Problems list */
|
||||
.problems-list .problem-item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.problem-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.problem-item a {
|
||||
text-decoration: none;
|
||||
color: #0077ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* Leaderboard */
|
||||
.leaderboard-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.leaderboard-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.leaderboard-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.leaderboard-table th,
|
||||
.leaderboard-table td {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
text-align: left;
|
||||
}
|
||||
.leaderboard-table th {
|
||||
background: #f9fafb;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.leaderboard-table tr:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
/* Sort indicators */
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.sortable::after {
|
||||
content: "↕";
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.8em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.sort-asc::after {
|
||||
content: "↑";
|
||||
opacity: 1;
|
||||
}
|
||||
.sort-desc::after {
|
||||
content: "↓";
|
||||
opacity: 1;
|
||||
}
|
||||
/* Toggle button */
|
||||
.btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
font-size: 0.85rem;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
.btn.active {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.content { grid-template-columns: 1fr; }
|
||||
.leaderboard-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,102 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Quick Problem Platform</title>
|
||||
<!--<link rel="favicon" href="/favicon/favicon.ico">-->
|
||||
<script src="script.js" async defer></script>
|
||||
<link rel="stylesheet" href="/static/index.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<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..."
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<section class="card" id="leaderboardSection">
|
||||
<div class="leaderboard-head">
|
||||
<h2 style="font-size:1.1rem;margin:0">Leaderboard</h2>
|
||||
<button class="btn" id="toggleLeaderboard">Hide</button>
|
||||
</div>
|
||||
<div class="leaderboard-controls">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="userSearch"
|
||||
placeholder="Filter by user..."
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="problemFilter"
|
||||
placeholder="Filter by problem..."
|
||||
/>
|
||||
<select class="filter-select" id="runtimeFilter">
|
||||
<option value="">All runtimes</option>
|
||||
<option value="best">Best runtime</option>
|
||||
<option value="worst">Worst runtime</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="leaderboardContainer">
|
||||
<table class="leaderboard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" data-sort="rank">Rank</th>
|
||||
<th class="sortable" data-sort="user">User</th>
|
||||
<th class="sortable" data-sort="problem">Problem</th>
|
||||
<th class="sortable" data-sort="runtime">Runtime (s)</th>
|
||||
<th class="sortable" data-sort="memory">Memory (KB)</th>
|
||||
<th>Line</th>
|
||||
<th class="sortable" data-sort="timestamp">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="leaderboardBody">
|
||||
{% for entry in leaderboard %}
|
||||
<tr data-user="{{ entry[0] }}" data-problem="{{ problem_titles.get(entry[1], 'Unknown') }}"
|
||||
data-runtime="{{ '%.4f'|format(entry[2]) }}" data-memory="{{ entry[3] }}"
|
||||
data-timestamp="{{ entry[5] }}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ entry[0] }}</td>
|
||||
<td>{{ 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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -1,152 +0,0 @@
|
||||
// Toggle leaderboard visibility
|
||||
const toggleBtn = document.getElementById('toggleLeaderboard');
|
||||
const leaderboardSection = document.getElementById('leaderboardSection');
|
||||
const contentContainer = document.getElementById('contentContainer');
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
if (leaderboardSection.style.display === 'none') {
|
||||
leaderboardSection.style.display = '';
|
||||
toggleBtn.textContent = 'Hide';
|
||||
contentContainer.classList.remove('single-column');
|
||||
} else {
|
||||
leaderboardSection.style.display = 'none';
|
||||
toggleBtn.textContent = 'Show';
|
||||
contentContainer.classList.add('single-column');
|
||||
}
|
||||
});
|
||||
|
||||
// Problem search functionality
|
||||
const problemSearch = document.getElementById('problemSearch');
|
||||
const problemsContainer = document.getElementById('problemsContainer');
|
||||
const problemItems = problemsContainer.querySelectorAll('.problem-item');
|
||||
|
||||
problemSearch.addEventListener('input', () => {
|
||||
const searchTerm = problemSearch.value.toLowerCase();
|
||||
problemItems.forEach(item => {
|
||||
const name = item.dataset.name.toLowerCase();
|
||||
const desc = item.dataset.desc?.toLowerCase() || '';
|
||||
if (name.includes(searchTerm) || desc.includes(searchTerm)) {
|
||||
item.style.display = '';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Leaderboard filtering and sorting
|
||||
const userSearch = document.getElementById('userSearch');
|
||||
const problemFilter = document.getElementById('problemFilter');
|
||||
const runtimeFilter = document.getElementById('runtimeFilter');
|
||||
const leaderboardBody = document.getElementById('leaderboardBody');
|
||||
const leaderboardRows = Array.from(leaderboardBody.querySelectorAll('tr'));
|
||||
const sortableHeaders = document.querySelectorAll('.sortable');
|
||||
|
||||
// Current sort state
|
||||
let currentSort = {
|
||||
column: null,
|
||||
direction: 'asc'
|
||||
};
|
||||
|
||||
// Filter leaderboard
|
||||
function filterLeaderboard() {
|
||||
const userTerm = userSearch.value.toLowerCase();
|
||||
const problemTerm = problemFilter.value.toLowerCase();
|
||||
const runtimeType = runtimeFilter.value;
|
||||
|
||||
leaderboardRows.forEach(row => {
|
||||
const user = row.dataset.user.toLowerCase();
|
||||
const problem = row.dataset.problem.toLowerCase();
|
||||
const runtime = parseFloat(row.dataset.runtime);
|
||||
const showUser = user.includes(userTerm);
|
||||
const showProblem = problem.includes(problemTerm);
|
||||
|
||||
let showRuntime = true;
|
||||
if (runtimeType === 'best') {
|
||||
// Find if this is the best runtime for this user+problem combo
|
||||
const userProblemRows = leaderboardRows.filter(r =>
|
||||
r.dataset.user === row.dataset.user &&
|
||||
r.dataset.problem === row.dataset.problem
|
||||
);
|
||||
const bestRuntime = Math.min(...userProblemRows.map(r => parseFloat(r.dataset.runtime)));
|
||||
showRuntime = runtime === bestRuntime;
|
||||
} else if (runtimeType === 'worst') {
|
||||
// Find if this is the worst runtime for this user+problem combo
|
||||
const userProblemRows = leaderboardRows.filter(r =>
|
||||
r.dataset.user === row.dataset.user &&
|
||||
r.dataset.problem === row.dataset.problem
|
||||
);
|
||||
const worstRuntime = Math.max(...userProblemRows.map(r => parseFloat(r.dataset.runtime)));
|
||||
showRuntime = runtime === worstRuntime;
|
||||
}
|
||||
|
||||
if (showUser && showProblem && showRuntime) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sort leaderboard
|
||||
function sortLeaderboard(column, direction) {
|
||||
const rows = Array.from(leaderboardBody.querySelectorAll('tr'));
|
||||
const index = Array.from(document.querySelectorAll('th')).findIndex(th => th.dataset.sort === column);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aValue = a.cells[index].textContent;
|
||||
let bValue = b.cells[index].textContent;
|
||||
|
||||
// Special handling for numeric columns
|
||||
if (column === 'runtime' || column === 'memory' || column === 'rank') {
|
||||
aValue = parseFloat(aValue) || 0;
|
||||
bValue = parseFloat(bValue) || 0;
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
// Special handling for timestamps
|
||||
if (column === 'timestamp') {
|
||||
aValue = new Date(aValue).getTime();
|
||||
bValue = new Date(bValue).getTime();
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
// Default string comparison
|
||||
aValue = aValue.toLowerCase();
|
||||
bValue = bValue.toLowerCase();
|
||||
if (aValue < bValue) return direction === 'asc' ? -1 : 1;
|
||||
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Re-append rows in sorted order
|
||||
rows.forEach(row => leaderboardBody.appendChild(row));
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
userSearch.addEventListener('input', filterLeaderboard);
|
||||
problemFilter.addEventListener('input', filterLeaderboard);
|
||||
runtimeFilter.addEventListener('change', filterLeaderboard);
|
||||
|
||||
// Set up sorting
|
||||
sortableHeaders.forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const column = header.dataset.sort;
|
||||
|
||||
// Reset all sort indicators
|
||||
sortableHeaders.forEach(h => {
|
||||
h.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
|
||||
// Determine new sort direction
|
||||
if (currentSort.column === column) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.column = column;
|
||||
currentSort.direction = 'asc';
|
||||
}
|
||||
|
||||
// Apply new sort
|
||||
header.classList.add(`sort-${currentSort.direction}`);
|
||||
sortLeaderboard(column, currentSort.direction);
|
||||
});
|
||||
});
|
||||
99
utils.py
99
utils.py
@@ -1,99 +0,0 @@
|
||||
import sys
|
||||
import traceback
|
||||
import time
|
||||
import io
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
def run_code_against_tests(user_code, test_code):
|
||||
local_ns = {}
|
||||
output = ''
|
||||
start = time.perf_counter()
|
||||
error = None
|
||||
passed = False
|
||||
temp_file = None
|
||||
|
||||
try:
|
||||
# Check if unittest is used in test_code
|
||||
if 'unittest' in test_code:
|
||||
# Write user code + test code to a temp file
|
||||
with tempfile.NamedTemporaryFile('w+', suffix='.py', delete=False, encoding='utf-8') as f:
|
||||
combined_code = f"{user_code}\n\n{test_code}"
|
||||
f.write(combined_code)
|
||||
f.flush()
|
||||
temp_file = f.name
|
||||
|
||||
# Run the file as a subprocess
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, temp_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
encoding='utf-8'
|
||||
)
|
||||
output = proc.stdout
|
||||
if proc.stderr:
|
||||
output += f"\n{proc.stderr}"
|
||||
|
||||
passed = proc.returncode == 0
|
||||
if not passed:
|
||||
error = f"Tests failed. Return code: {proc.returncode}\n{output}"
|
||||
else:
|
||||
# For successful unittest runs, the stderr contains the test results
|
||||
if proc.stderr and "OK" in proc.stderr:
|
||||
output = proc.stderr # Use stderr as the main output for unittest
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
passed = False
|
||||
error = "Code execution timed out after 10 seconds"
|
||||
output = "Execution timed out"
|
||||
|
||||
else:
|
||||
# Capture stdout
|
||||
old_stdout = sys.stdout
|
||||
captured_output = io.StringIO()
|
||||
sys.stdout = captured_output
|
||||
|
||||
try:
|
||||
# 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 AssertionError as e:
|
||||
passed = False
|
||||
error = f"Assertion failed: {str(e)}"
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Runtime error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
output = captured_output.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Execution error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not delete temp file {temp_file}: {e}")
|
||||
|
||||
runtime = time.perf_counter() - start
|
||||
|
||||
result = {
|
||||
'passed': passed,
|
||||
'output': output.strip() if output else '',
|
||||
'runtime': runtime,
|
||||
'error': error if not passed else None
|
||||
}
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user