902 lines
34 KiB
Python
902 lines
34 KiB
Python
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() |