Commit 35f66844 authored by David Paul's avatar David Paul
Browse files

Upgrade to newer versions of libraries and switch to Monaco code editor

parent 4dfd9d89
...@@ -25,4 +25,7 @@ Network Trash Folder ...@@ -25,4 +25,7 @@ Network Trash Folder
Temporary Items Temporary Items
.apdisk .apdisk
bower_components/* node_modules/*
package-lock.json
__pycache__/*
MIT License MIT License
Copyright (c) 2016 David John Paul Copyright (c) 2016-2022 David John Paul
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
......
...@@ -3,10 +3,13 @@ from browser import alert ...@@ -3,10 +3,13 @@ from browser import alert
from browser import document from browser import document
from browser import html from browser import html
from browser import window from browser import window
jq = window.jQuery.noConflict(True) jq = window.jQuery.noConflict(True)
window.jq = jq; window.jq = jq;
editor = window.editor editor = window.editor
def echo_prompt(prompt = ""): def echo_prompt(prompt = ""):
"""Function to make prompt to input appear properly""" """Function to make prompt to input appear properly"""
print(prompt, end = "") print(prompt, end = "")
...@@ -14,17 +17,27 @@ def echo_prompt(prompt = ""): ...@@ -14,17 +17,27 @@ def echo_prompt(prompt = ""):
print(value, end="\n") print(value, end="\n")
return value return value
def fake_input(prompt = ""):
"""Function that avoids input by directly reading standard input, and also echos the prompt"""
print(prompt, end = "")
value = sys.stdin.readline()
print(value, end = "\n")
return value
class Output: class Output:
"""Writes output to #output""" """Writes output to #output"""
def write(self, *args): def write(self, *args):
for arg in args: for arg in args:
jq("#output").val(jq("#output").val() + arg) jq("#output").val(jq("#output").val() + arg)
def display_exercise(exercise): def display_exercise(exercise):
"""Displays the given exercise""" """Displays the given exercise"""
window.exercise.html = exercise.title window.exercise.html = exercise.title
window.instructions.html = exercise.instructions window.instructions.html = exercise.instructions
window.editor.setValue(exercise.code, -1) window.editor.setValue(exercise.code)
window.tests.clear() window.tests.clear()
for test_id in range(len(exercise.tests)): for test_id in range(len(exercise.tests)):
test = exercise.tests[test_id] test = exercise.tests[test_id]
...@@ -35,8 +48,9 @@ def display_exercise(exercise): ...@@ -35,8 +48,9 @@ def display_exercise(exercise):
list_element.bind("click", show_test) list_element.bind("click", show_test)
list_element.bind("dblclick", run_test) list_element.bind("dblclick", run_test)
window.tests <= list_element window.tests <= list_element
jq("#output-row").addClass("hidden") jq("#output-row").addClass("visually-hidden")
jq("#test-row").addClass("hidden") jq("#test-row").addClass("visually-hidden")
def update_tests(remove_active = False): def update_tests(remove_active = False):
"""Checks if all tests have passed, removing the active class if requested""" """Checks if all tests have passed, removing the active class if requested"""
...@@ -56,32 +70,38 @@ def update_tests(remove_active = False): ...@@ -56,32 +70,38 @@ def update_tests(remove_active = False):
if all_passed: if all_passed:
jq("#grade").removeClass("disabled") jq("#grade").removeClass("disabled")
def display_test(id): def display_test(id):
"""Displays the test with the given id""" """Displays the test with the given id"""
update_tests(True) update_tests(True)
jq("#output-row").addClass("hidden") jq("#output-row").addClass("visually-hidden")
jq("#test-row").removeClass("hidden") jq("#test-row").removeClass("visually-hidden")
jq("#test_%d" % id).addClass("active") jq("#test_%d" % id).addClass("active")
test = exercise.tests[id] test = exercise.tests[id]
jq("#expected").val(test.expected_output) jq("#expected").val(test.expected_output)
jq("#actual").val(test.actual_output) jq("#actual").val(test.actual_output)
jq("#input").val(test.input) jq("#input").val(test.input)
window.updateDiff()
def show_test(ev): def show_test(ev):
"""Displays the test that was selected""" """Displays the test that was selected"""
id = int(ev.target.id[len("test_"):]) id = int(ev.target.id[len("test_"):])
display_test(id) display_test(id)
def run_test(ev): def run_test(ev):
"""Runs the test that was selected""" """Runs the test that was selected"""
id = int(ev.target.id[len("test_"):]) id = int(ev.target.id[len("test_"):])
execute_test(exercise.tests[id]) execute_test(exercise.tests[id])
display_test(id) display_test(id)
def get_code(): def get_code():
"""Gets the code currently in the editor""" """Gets the code currently in the editor"""
return window.editor.getValue() return window.editor.getValue()
def display_checks(code): def display_checks(code):
"""Displays alerts for any check that fails - returns True if all checks pass, False otherwise""" """Displays alerts for any check that fails - returns True if all checks pass, False otherwise"""
for check in exercise.checks: for check in exercise.checks:
...@@ -91,54 +111,62 @@ def display_checks(code): ...@@ -91,54 +111,62 @@ def display_checks(code):
return False return False
return True return True
def execute_test(test): def execute_test(test):
"""Runs the code to complete a test""" """Runs the code to complete a test"""
code = get_code() code = get_code()
if not display_checks(code): if not display_checks(code):
return return
test.run(code, echo_prompt) test.run(code, fake_input)
update_tests() update_tests()
def execute_run(*args): def execute_run(*args):
"""Runs the code for the user to interact with""" """Runs the code for the user to interact with"""
jq("#test-row").addClass("hidden") jq("#test-row").addClass("visually-hidden")
jq("#output-row").removeClass("hidden") jq("#output-row").removeClass("visually-hidden")
jq("#output").val("") jq("#output").val("")
code = get_code() code = get_code()
execute(code, output, output, sys.stdin, echo_prompt) execute(code, output, output, sys.stdin, echo_prompt)
def execute_tests(*args): def execute_tests(*args):
"""Executes each test for the exercise""" """Executes each test for the exercise"""
code = get_code() code = get_code()
if not display_checks(code): if not display_checks(code):
return return
exercise.run(code, echo_prompt) exercise.run(code, fake_input)
display_test(0) display_test(0)
def grade_code(): def grade_code():
if jq("#grade").hasClass("disabled"): if jq("#grade").hasClass("disabled"):
alert("Run all tests successfully to allow grade upload") alert("Run all tests successfully to allow grade upload")
else: else:
window.submitGrade(); window.submitGrade();
def code_change(*args): def code_change(*args):
"""Handles any time the code changes - test results become invalid""" """Handles any time the code changes - test results become invalid"""
jq("#warning").text("") jq("#warning").text("")
jq("#output-row").addClass("hidden") jq("#output-row").addClass("visually-hidden")
jq("#test-row").addClass("hidden") jq("#test-row").addClass("visually-hidden")
for test in exercise.tests: for test in exercise.tests:
test.actual_output = "" test.actual_output = ""
update_tests(True) update_tests(True)
output = Output() output = Output()
exercise = get_exercise(window.exercise_id)
window.exercise.html = "No Exercise Specified" window.exercise.html = "No Exercise Specified"
window.instructions.html = "No exercise was specified, or the exercise is unavailable. Please contact your instructor." window.instructions.html = "No exercise was specified, or the exercise is unavailable. Please contact your instructor."
exercise = get_exercise(window.exercise_id)
display_exercise(exercise) display_exercise(exercise)
document["run"].bind("click", execute_run) document["run"].bind("click", execute_run)
document["test-all"].bind("click", execute_tests) document["test-all"].bind("click", execute_tests)
document["grade"].bind("click", grade_code) document["grade"].bind("click", grade_code)
editor.on("input", code_change) editor.onDidChangeModelContent(code_change)
...@@ -2,6 +2,7 @@ from exercise import * ...@@ -2,6 +2,7 @@ from exercise import *
from exercises import * from exercises import *
import sys import sys
def get_exercise(id = 0): def get_exercise(id = 0):
"""Loads the exercise to be completed""" """Loads the exercise to be completed"""
try: try:
...@@ -13,14 +14,17 @@ def get_exercise(id = 0): ...@@ -13,14 +14,17 @@ def get_exercise(id = 0):
return exercises[id] return exercises[id]
original_input = input original_input = input
def output_input(prompt = ""): def output_input(prompt = ""):
"""A function to output the value that is input before it is returned.""" """A function to output the value that is input before it is returned."""
value = original_input(prompt) value = original_input(prompt)
print(value) print(value)
return value return value
if __name__ == "__main__": if __name__ == "__main__":
exercise_id = sys.argv[-2] exercise_id = sys.argv[-2]
exercise = get_exercise(exercise_id) exercise = get_exercise(exercise_id)
......
{
"name": "python-automarker",
"authors": [
"David Paul <david@davidjohnpaul.com>"
],
"description": "A Python 3 AutoMarker",
"main": "",
"keywords": [
"python",
"automarker"
],
"license": "MIT",
"homepage": "https://bitbucket.org/davidjohnpaul/automarker",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"bootstrap": "^3.3.6",
"ace": "git://github.com/ajaxorg/ace-builds.git#^1.2.3",
"pythonauto": "https://github.com/csev/pythonauto.git",
"html5shiv": "^3.7.3",
"Brython-3.3.0.tar": "https://github.com/brython-dev/brython/releases/download/3.3.0/Brython-3.3.0.tar.gz"
}
}
...@@ -5,8 +5,9 @@ import traceback ...@@ -5,8 +5,9 @@ import traceback
# Pattern to remove Traceback from before an exec call # Pattern to remove Traceback from before an exec call
error_pattern = ".*?\$exec_\d+.*? " error_pattern = ".*?\$exec_\d+.*? "
regex_error = re.compile(error_pattern, re.DOTALL) regex_error = re.compile(error_pattern, re.DOTALL)
def execute(code, stdout = sys.stdout, stderr = sys.stderr, stdin = sys.stdin, input = input):
def execute(code, stdout = sys.stdout, stderr = sys.stderr, stdin = sys.stdin, new_input = input):
"""Executes the given code, with standard output and standard error going to the given values""" """Executes the given code, with standard output and standard error going to the given values"""
old_stdout = sys.stdout old_stdout = sys.stdout
old_stderr = sys.stderr old_stderr = sys.stderr
...@@ -18,35 +19,38 @@ def execute(code, stdout = sys.stdout, stderr = sys.stderr, stdin = sys.stdin, i ...@@ -18,35 +19,38 @@ def execute(code, stdout = sys.stdout, stderr = sys.stderr, stdin = sys.stdin, i
try: try:
available_vars = {} available_vars = {}
available_vars["input"] = input available_vars["input"] = new_input
exec(code, available_vars) exec(code, available_vars)
return True return True
except Exception as exc: except Exception as exc:
msg = traceback.format_exc() exc.filename = "automarker.py"
msg = regex_error.sub("", msg, 1) traceback.print_exception(exc)
print(msg)
return False return False
finally: finally:
sys.stdout = old_stdout sys.stdout = old_stdout
sys.stderr = old_stderr sys.stderr = old_stderr
sys.stdin = old_stdin sys.stdin = old_stdin
class Test: class Test:
"""A test to run over the given code""" """A test to run over the given code"""
def __init__(self, name, expected_output = "", input = ""):
def __init__(self, name, expected_output = "", test_input = ""):
"""Gives the test the give name, expected output, and input that it will provide when run""" """Gives the test the give name, expected output, and input that it will provide when run"""
self.name = name self.name = name
self.expected_output = expected_output self.expected_output = expected_output
self.actual_output = "" self.actual_output = ""
self.input = input self.input = test_input
self.remaining_input = "" self.remaining_input = ""
def write(self, *args): def write(self, *args):
"""Writes the output to the test object""" """Writes the output to the test object"""
for arg in args: for arg in args:
self.actual_output += arg self.actual_output += arg
def read(self, *args): def read(self, *args):
"""Reads the input from the test object""" """Reads the input from the test object"""
if len(self.remaining_input) <= 0: if len(self.remaining_input) <= 0:
...@@ -55,32 +59,38 @@ class Test: ...@@ -55,32 +59,38 @@ class Test:
self.remaining_input = self.remaining_input[1:] self.remaining_input = self.remaining_input[1:]
return ret_val return ret_val
def readline(self, *args): def readline(self, *args):
"""Reads the input from the test object""" """Reads the input from the test object"""
remaining = self.remaining_input.partition("\n") remaining = self.remaining_input.partition("\n")
self.remaining_input = remaining[2] self.remaining_input = remaining[2]
return remaining[0] return remaining[0]
def close(self, *args): def close(self, *args):
"""Allow Test to be stdin""" """Allow Test to be stdin"""
pass pass
def run(self, code, input = input):
def run(self, code, new_input = input):
"""Executes this test, returning True if the test passes (False otherwise)""" """Executes this test, returning True if the test passes (False otherwise)"""
self.remaining_input = self.input self.remaining_input = self.input
self.actual_output = "" self.actual_output = ""
execute(code, self, self, self, input) execute(code, self, self, self, new_input)
return self.actual_output == self.expected_output return self.actual_output == self.expected_output
class Check: class Check:
"""Performs a check on the code""" """Performs a check on the code"""
def __init__(self, text, regex, ensure_no_match = False): def __init__(self, text, regex, ensure_no_match = False):
"""Sets the text and the pattern to match""" """Sets the text and the pattern to match"""
self.text = text self.text = text
self.regex = re.compile(regex) self.regex = re.compile(regex)
self.ensure_no_match = ensure_no_match self.ensure_no_match = ensure_no_match
def check(self, code): def check(self, code):
"""Ensures this check succeeds""" """Ensures this check succeeds"""
match = self.regex.search(code) match = self.regex.search(code)
...@@ -88,9 +98,11 @@ class Check: ...@@ -88,9 +98,11 @@ class Check:
return self.ensure_no_match return self.ensure_no_match
return not self.ensure_no_match return not self.ensure_no_match
class Exercise: class Exercise:
"""The exercise to be completed""" """The exercise to be completed"""
def __init__(self, title, instructions = "", code = "", tests = [], checks = []): def __init__(self, title, instructions = "", code = "", tests = [], checks = []):
"""Sets the title, instructions, initial code, tests, and checks to be performed to complete this exercise""" """Sets the title, instructions, initial code, tests, and checks to be performed to complete this exercise"""
self.title = title self.title = title
...@@ -98,6 +110,8 @@ class Exercise: ...@@ -98,6 +110,8 @@ class Exercise:
self.code = code self.code = code
self.tests = tests self.tests = tests
self.checks = checks self.checks = checks
self.filename = "Exercise"
def run_checks(self, code): def run_checks(self, code):
"""Ensures all checks pass on the given code, returning True if all checks succeed (False otherwise)""" """Ensures all checks pass on the given code, returning True if all checks succeed (False otherwise)"""
...@@ -106,13 +120,15 @@ class Exercise: ...@@ -106,13 +120,15 @@ class Exercise:
all_succeeded = check.check(code) and all_succeeded all_succeeded = check.check(code) and all_succeeded
return all_succeeded return all_succeeded
def run_tests(self, code, input = input):
def run_tests(self, code, new_input = input):
"""Runs all tests in this exercise, returning True if all succeed (False otherwise)""" """Runs all tests in this exercise, returning True if all succeed (False otherwise)"""
all_succeeded = True all_succeeded = True
for test in self.tests: for test in self.tests:
all_succeeded = test.run(code, input) and all_succeeded all_succeeded = test.run(code, new_input) and all_succeeded
return all_succeeded return all_succeeded
def run(self, code, input = input):
def run(self, code, new_input = input):
"""Runs all checks and, if they succeed, all tests, returning True if all checks and tests succeed (False otherwise)""" """Runs all checks and, if they succeed, all tests, returning True if all checks and tests succeed (False otherwise)"""
return self.run_checks(code) and self.run_tests(code, input) return self.run_checks(code) and self.run_tests(code, new_input)
<?php <?php
# Based on https://github.com/csev/pythonauto/blob/master/grade.php # Based on https://github.com/csev/pythonauto/blob/master/grade.php
require_once('bower_components/pythonauto/util/lti_util.php'); require_once('lti_util/lti_util.php');
session_start(); session_start();
if (isset($_REQUEST["exercise"]) && preg_match("/^\d+$/", $_REQUEST["exercise"])) { if (isset($_REQUEST["exercise"]) && preg_match("/^\d+$/", $_REQUEST["exercise"])) {
......
<?php <?php
session_start(); session_start();
require_once("bower_components/pythonauto/util/lti_util.php"); require_once("lti_util/lti_util.php");
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Python Automarker</title> <title>Python Automarker</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen"> <link href="node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen" />
<link href="css/automarker.css" rel="stylesheet" media="screen"> <link href="css/automarker.css" rel="stylesheet" media="screen" />
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="bower_components/html5shiv/dist/html5shiv-printshiv.min.js"></script> <script src="node_modules/html5shiv/dist/html5shiv-printshiv.min.js"></script>
<![endif]--> <![endif]-->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script> <script type="text/javascript" src="node_modules/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script> <script type="text/javascript" src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/Brython3.3.0.tar/brython.js"></script> <script type="text/javascript" src="node_modules/brython/brython.min.js"></script>
<script type="text/javascript" src="bower_components/Brython3.3.0.tar/brython_stdlib.js"></script> <script type="text/javascript" src="node_modules/brython/brython_stdlib.js"></script>
<script type="text/javascript" src="bower_components/ace/src-min-noconflict/ace.js"></script> <script type="text/javascript" src="node_modules/monaco-editor/min/vs/loader.js"></script>
<script type="text/javascript"> <script type="text/javascript">
exercise_id = <?php echo isset($_REQUEST["exercise_id"]) ? $_REQUEST["exercise_id"] : 0; ?>; exercise_id = <?php echo isset($_REQUEST["exercise_id"]) ? $_REQUEST["exercise_id"] : 0; ?>;
editor = document.getElementById('editor');
jQuery(document).ready(function() { jQuery(document).ready(function() {
editor = ace.edit("editor"); require.config({ paths: { vs: 'node_modules/monaco-editor/min/vs' } });
editor.session.setMode("ace/mode/python"); require(['vs/editor/editor.main'], function () {
editor.$blockScrolling = Infinity; editor = monaco.editor.create(document.getElementById('editor'), {value: "", language: 'python', minimap: {enabled: false}});
brython(); diffEditor = monaco.editor.createDiffEditor(document.getElementById('diffEditor'));
brython();
});
}); });
</script> </script>
</head> </head>