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
Temporary Items
.apdisk
bower_components/*
node_modules/*
package-lock.json
__pycache__/*
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
of this software and associated documentation files (the "Software"), to deal
......
......@@ -3,10 +3,13 @@ from browser import alert
from browser import document
from browser import html
from browser import window
jq = window.jQuery.noConflict(True)
window.jq = jq;
editor = window.editor
def echo_prompt(prompt = ""):
"""Function to make prompt to input appear properly"""
print(prompt, end = "")
......@@ -14,17 +17,27 @@ def echo_prompt(prompt = ""):
print(value, end="\n")
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:
"""Writes output to #output"""
def write(self, *args):
for arg in args:
jq("#output").val(jq("#output").val() + arg)
def display_exercise(exercise):
"""Displays the given exercise"""
window.exercise.html = exercise.title
window.instructions.html = exercise.instructions
window.editor.setValue(exercise.code, -1)
window.editor.setValue(exercise.code)
window.tests.clear()
for test_id in range(len(exercise.tests)):
test = exercise.tests[test_id]
......@@ -35,8 +48,9 @@ def display_exercise(exercise):
list_element.bind("click", show_test)
list_element.bind("dblclick", run_test)
window.tests <= list_element
jq("#output-row").addClass("hidden")
jq("#test-row").addClass("hidden")
jq("#output-row").addClass("visually-hidden")
jq("#test-row").addClass("visually-hidden")
def update_tests(remove_active = False):
"""Checks if all tests have passed, removing the active class if requested"""
......@@ -56,32 +70,38 @@ def update_tests(remove_active = False):
if all_passed:
jq("#grade").removeClass("disabled")
def display_test(id):
"""Displays the test with the given id"""
update_tests(True)
jq("#output-row").addClass("hidden")
jq("#test-row").removeClass("hidden")
jq("#output-row").addClass("visually-hidden")
jq("#test-row").removeClass("visually-hidden")
jq("#test_%d" % id).addClass("active")
test = exercise.tests[id]
jq("#expected").val(test.expected_output)
jq("#actual").val(test.actual_output)
jq("#input").val(test.input)
window.updateDiff()
def show_test(ev):
"""Displays the test that was selected"""
id = int(ev.target.id[len("test_"):])
display_test(id)
def run_test(ev):
"""Runs the test that was selected"""
id = int(ev.target.id[len("test_"):])
execute_test(exercise.tests[id])
display_test(id)
def get_code():
"""Gets the code currently in the editor"""
return window.editor.getValue()
def display_checks(code):
"""Displays alerts for any check that fails - returns True if all checks pass, False otherwise"""
for check in exercise.checks:
......@@ -91,54 +111,62 @@ def display_checks(code):
return False
return True
def execute_test(test):
"""Runs the code to complete a test"""
code = get_code()
if not display_checks(code):
return
test.run(code, echo_prompt)
test.run(code, fake_input)
update_tests()
def execute_run(*args):
"""Runs the code for the user to interact with"""
jq("#test-row").addClass("hidden")
jq("#output-row").removeClass("hidden")
jq("#test-row").addClass("visually-hidden")
jq("#output-row").removeClass("visually-hidden")
jq("#output").val("")
code = get_code()
execute(code, output, output, sys.stdin, echo_prompt)
def execute_tests(*args):
"""Executes each test for the exercise"""
code = get_code()
if not display_checks(code):
return
exercise.run(code, echo_prompt)
exercise.run(code, fake_input)
display_test(0)
def grade_code():
if jq("#grade").hasClass("disabled"):
alert("Run all tests successfully to allow grade upload")
else:
window.submitGrade();
def code_change(*args):
"""Handles any time the code changes - test results become invalid"""
jq("#warning").text("")
jq("#output-row").addClass("hidden")
jq("#test-row").addClass("hidden")
jq("#output-row").addClass("visually-hidden")
jq("#test-row").addClass("visually-hidden")
for test in exercise.tests:
test.actual_output = ""
update_tests(True)
output = Output()
exercise = get_exercise(window.exercise_id)
window.exercise.html = "No Exercise Specified"
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)
document["run"].bind("click", execute_run)
document["test-all"].bind("click", execute_tests)
document["grade"].bind("click", grade_code)
editor.on("input", code_change)
editor.onDidChangeModelContent(code_change)
......@@ -2,6 +2,7 @@ from exercise import *
from exercises import *
import sys
def get_exercise(id = 0):
"""Loads the exercise to be completed"""
try:
......@@ -13,14 +14,17 @@ def get_exercise(id = 0):
return exercises[id]
original_input = input
def output_input(prompt = ""):
"""A function to output the value that is input before it is returned."""
value = original_input(prompt)
print(value)
return value
if __name__ == "__main__":
exercise_id = sys.argv[-2]
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"
}
}
......@@ -6,7 +6,8 @@ import traceback
error_pattern = ".*?\$exec_\d+.*? "
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"""
old_stdout = sys.stdout
old_stderr = sys.stderr
......@@ -18,35 +19,38 @@ def execute(code, stdout = sys.stdout, stderr = sys.stderr, stdin = sys.stdin, i
try:
available_vars = {}
available_vars["input"] = input
available_vars["input"] = new_input
exec(code, available_vars)
return True
except Exception as exc:
msg = traceback.format_exc()
msg = regex_error.sub("", msg, 1)
print(msg)
exc.filename = "automarker.py"
traceback.print_exception(exc)
return False
finally:
sys.stdout = old_stdout
sys.stderr = old_stderr
sys.stdin = old_stdin
class Test:
"""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"""
self.name = name
self.expected_output = expected_output
self.actual_output = ""
self.input = input
self.input = test_input
self.remaining_input = ""
def write(self, *args):
"""Writes the output to the test object"""
for arg in args:
self.actual_output += arg
def read(self, *args):
"""Reads the input from the test object"""
if len(self.remaining_input) <= 0:
......@@ -55,32 +59,38 @@ class Test:
self.remaining_input = self.remaining_input[1:]
return ret_val
def readline(self, *args):
"""Reads the input from the test object"""
remaining = self.remaining_input.partition("\n")
self.remaining_input = remaining[2]
return remaining[0]
def close(self, *args):
"""Allow Test to be stdin"""
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)"""
self.remaining_input = self.input
self.actual_output = ""
execute(code, self, self, self, input)
execute(code, self, self, self, new_input)
return self.actual_output == self.expected_output
class Check:
"""Performs a check on the code"""
def __init__(self, text, regex, ensure_no_match = False):
"""Sets the text and the pattern to match"""
self.text = text
self.regex = re.compile(regex)
self.ensure_no_match = ensure_no_match
def check(self, code):
"""Ensures this check succeeds"""
match = self.regex.search(code)
......@@ -88,9 +98,11 @@ class Check:
return self.ensure_no_match
return not self.ensure_no_match
class Exercise:
"""The exercise to be completed"""
def __init__(self, title, instructions = "", code = "", tests = [], checks = []):
"""Sets the title, instructions, initial code, tests, and checks to be performed to complete this exercise"""
self.title = title
......@@ -98,6 +110,8 @@ class Exercise:
self.code = code
self.tests = tests
self.checks = checks
self.filename = "Exercise"
def run_checks(self, code):
"""Ensures all checks pass on the given code, returning True if all checks succeed (False otherwise)"""
......@@ -106,13 +120,15 @@ class Exercise:
all_succeeded = check.check(code) and 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)"""
all_succeeded = True
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
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)"""
return self.run_checks(code) and self.run_tests(code, input)
return self.run_checks(code) and self.run_tests(code, new_input)
<?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();
if (isset($_REQUEST["exercise"]) && preg_match("/^\d+$/", $_REQUEST["exercise"])) {
......
<?php
session_start();
require_once("bower_components/pythonauto/util/lti_util.php");
require_once("lti_util/lti_util.php");
?>
<!DOCTYPE html>
<html>
<head>
<title>Python Automarker</title>
<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="css/automarker.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" />
<!--[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]-->
<script type="text/javascript" src="bower_components/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="bower_components/Brython3.3.0.tar/brython.js"></script>
<script type="text/javascript" src="bower_components/Brython3.3.0.tar/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/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="node_modules/brython/brython.min.js"></script>
<script type="text/javascript" src="node_modules/brython/brython_stdlib.js"></script>
<script type="text/javascript" src="node_modules/monaco-editor/min/vs/loader.js"></script>
<script type="text/javascript">
exercise_id = <?php echo isset($_REQUEST["exercise_id"]) ? $_REQUEST["exercise_id"] : 0; ?>;
editor = document.getElementById('editor');
jQuery(document).ready(function() {
editor = ace.edit("editor");
editor.session.setMode("ace/mode/python");
editor.$blockScrolling = Infinity;
require.config({ paths: { vs: 'node_modules/monaco-editor/min/vs' } });
require(['vs/editor/editor.main'], function () {
editor = monaco.editor.create(document.getElementById('editor'), {value: "", language: 'python', minimap: {enabled: false}});
diffEditor = monaco.editor.createDiffEditor(document.getElementById('diffEditor'));
brython();
});
});
</script>
</head>
<body class="container">
<div class="row" id="header-row">
<div class="col-md-12 panel-primary">
<h1 id="exercise" class="panel-heading">Loading...</h1>
<p id="instructions" class="panel-body">Loading...</p>
</div>
</div>
<div class="row" id="code-row">
<div class="col-md-8">
<pre id="editor" style="height: 240px;"></pre>
<div class="accordian col-md-12" id="accordianMain">
<!-- Instructions -->
<div class="accordian-item">
<h2 class="accordian-header" id="headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<h1 id="exercise">Loading...</h1>
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordionMain">
<div class="accordion-body">
<span id="instructions">Loading...</span>
</div>
<div class="col-md-4">
<h3 class="text-center">Tests</h3>
<div id="tests" class="list-group"></div>
</div>
</div>
<div class="row" id="controls-row">
<div class="col-md-12 text-center">
<!-- Code -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo">
Code
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse show" aria-labelledby="headingTwo" data-bs-parent="#accordionMain">
<div class="accordion-body">
<div id="editor" style="height: 400px;"></div>
<div class="form-group text-center">
<button id="run" class="btn btn-secondary">Run</button>
<button id="test-all" type="button" class="btn btn-primary">Run Tests</button>
<button id="grade" type="button" class="btn btn-success disabled">Grade</button>
<span id="nograde" class="visually-hidden">Connect through a LMS to submit grade information</span>
</div>
<div><span id="warning"></span></div>
<div class="visually-hidden" id="output-row">
<h3>Output</h3>
<div class="form-group">
<button id="run" class="btn">Run</button>
<button id="test-all" type="button" class="btn">Run Tests</button>
<button id="grade" type="button" class="btn disabled">Grade</button>
<span id="nograde" class="hidden">Connect through a LMS to submit grade information</span>
</div>
<textarea id="output" readonly class="form-control"></textarea>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<span id="warning"></span>
</div>
</div>
<div class="row hidden" id="output-row">
<div class="col-md-12">
<h3 class="text-center">Output</h3>
<div id="container" style="width: 100%"></div>
<textarea id="output" readonly class="form-control" style="height: 240px"></textarea>
</div>
<!-- Tests -->
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="true" aria-controlls="collapseThree">
Tests
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse show" aria-labelledby="headingThree" data-bs-parent="#accordionMain">
<div class="accordion-body">
<div id="tests" class="list-group"></div>
<div class="visually-hidden" id="test-row">
<h3>Comparison of Expected and Actual Output</h3>
<div class="form-group">
<div id="diffEditor" style="height:400px; width: 800px;" class="form-control"></div>
</div>
<div class="row hidden" id="test-row">
<div class="col-md-12 text-center">
<h3>Expected Output</h3>
<div class="form-group">
<textarea id="expected" readonly class="form-control"></textarea>
......@@ -87,18 +105,24 @@ require_once("bower_components/pythonauto/util/lti_util.php");
<div class="form-group">
<textarea id="input" readonly class="form-control"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/python" src="automarker_client.py"></script>
<script type="text/javascript">
submission_url = '';
redirect_url = '';
<?php
if (!is_lti_request()) {
echo " jQuery('#grade').addClass('hidden');\n";
echo " jQuery('#nograde').removeClass('hidden');\n";
echo "jQuery(document).ready(function() {\n";
echo " jQuery('#grade').addClass('visually-hidden');\n";
echo " jQuery('#nograde').removeClass('visually-hidden');\n";
echo " jQuery('#nograde').show();\n";
echo "});\n";
} else {
$oauth_consumer_key = "";
$oauth_consumer_secret = "";
......@@ -111,9 +135,11 @@ require_once("bower_components/pythonauto/util/lti_util.php");
}
$context = new BLTI($oauth_consumer_secret, true, false);
if (!$context->valid) {
echo " jQuery('#grade').addClass('hidden');\n";
echo " jQuery('#nograde').removeClass('hidden');\n";
echo " jQuery(document).ready(function() {\n";
echo " jQuery('#grade').addClass('visually-hidden');\n";
echo " jQuery('#nograde').removeClass('visually-hidden');\n";
echo " jQuery('#nograde').show();\n";
echo " });\n";
} else {
echo " submission_url = '" . $context->addSession("grade.php") . "';\n";
if (isset($_POST['launch_presentation_return_url'])) {
......@@ -142,6 +168,14 @@ require_once("bower_components/pythonauto/util/lti_util.php");
alert("Connect through a LMS to submit grade information");
}
}
function updateDiff() {
diffEditor.setModel({
original: monaco.editor.createModel(document.getElementById("expected").value, "text/plain"),
modified: monaco.editor.createModel(document.getElementById("actual").value, "text/plain")
});
}
</script>
<script type="text/python" src="automarker_client.py"></script>
</body>
</html>
The MIT License
Copyright (c) 2007 Andy Smith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This diff is collapsed.
This diff is collapsed.
{
"name": "python-automarker",
"version": "3.0.0",
"description": "A Python automarker developed for COSC110 at the University of New England, Australia",
"dependencies": {
"bootstrap": "^5.1.3",
"brython": "^3.10.5",
"html5shiv": "^3.7.3",
"jquery": "^3.6.0",
"monaco-editor": "^0.32.1"
}
}
......@@ -2,7 +2,7 @@
An [LTI](http://www.imsglobal.org/activity/learning-tools-interoperability)-based Python autograder similar to [pythonauto](https://github.com/csev/pythonauto), but using [Brython](http://http://brython.info/) to support Python 3.
This is a very simple project, requiring PHP support on the server-side. All dependencies are managed by [Bower](https://bower.io/), so after cloning you should run `bower install`.
This project requires PHP support on the server-side. All dependencies are managed by [npm](https://www.npmjs.com/), so after cloning you should run `npm install`.